利用Swift的协议扩展和泛型实现重用
作为一名开发者,常见的任务之一是通过子类化自定义单元格来实现 UITableView 或 UICollectionView 的定制。在注册自定义单元格子类方面,UITableView
和UICollectionView
提供了非常相似的 API。
public func registerClass(cellClass: AnyClass?, forCellWithReuseIdentifier identifier: String)
public func registerNib(nib: UINib?, forCellWithReuseIdentifier identifier: String)
注册自定义单元格的常用方法是声明一个常量 reuseIdentifier,如下所示:
private let reuseIdentifier = "BookCell"
class BookListViewController: UIViewController, UICollectionViewDataSource {
@IBOutlet private weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "BookCell", bundle: nil)
self.collectionView.registerNib(nib, forCellWithReuseIdentifier: reuseIdentifier)
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath)
if let bookCell = cell as? BookCell {
// TODO: 配置单元格
}
return cell
}
}
接下来,我们尝试使用泛型来简化代码并提高安全性。
首先,如果我们不需要在代码中到处声明重用标识符常量,那将非常理想。实际上,我们可以直接使用自定义单元格的类名作为默认的重用标识符。
我们可以创建一个名为 ReusableViews 的协议,并为 UIView 的子类创建一个默认声明方法。
protocol ReusableView: AnyObject {
static var defaultReuseIdentifier: String { get }
}
extension ReusableView where Self: UIView {
static var defaultReuseIdentifier: String {
return NSStringFromClass(self)
}
}
extension UICollectionViewCell: ReusableView {
}
通过让UICollectionViewCell
遵循ReusableView
协议,我们可以为每个单元格子类获取一个唯一的重用标识符。
let identifier = BookCell.defaultReuseIdentifier
// identifier = "MyModule.BookCell"
接下来,我们将使用相同的方法简化注册 Nib 的步骤。
我们创建一个名为Nib Loadable Views的协议,并通过协议扩展添加默认方法实现。
protocol NibLoadableView: AnyObject {
static var nibName: String { get }
}
extension NibLoadableView where Self: UIView {
static var nibName: String {
return NSStringFromClass(self).components(separatedBy: ".").last!
}
}
extension BookCell: NibLoadableView {
}
通过让我们的BookCell
类遵循NibLoadableView
协议,我们现在可以更安全、更方便地获取 Nib 的名称。
let nibName = BookCell.nibName
// nibName = "BookCell"
有了这两个协议,我们可以利用Swift 的泛型和扩展功能来简化单元格的注册和使用。
extension UICollectionView {
func register<T: UICollectionViewCell>(_: T.Type) where T: ReusableView {
register(T.self, forCellWithReuseIdentifier: T.defaultReuseIdentifier)
}
func register<T: UICollectionViewCell>(_: T.Type) where T: ReusableView & NibLoadableView {
let bundle = Bundle(for: T.self)
let nib = UINib(nibName: T.nibName, bundle: bundle)
register(nib, forCellWithReuseIdentifier: T.defaultReuseIdentifier)
}
func dequeueReusableCell<T: UICollectionViewCell>(for indexPath: IndexPath) -> T where T: ReusableView {
guard let cell = dequeueReusableCell(withReuseIdentifier: T.defaultReuseIdentifier, for: indexPath) as? T else {
fatalError("Could not dequeue cell with identifier: \(T.defaultReuseIdentifier)")
}
return cell
}
}
请注意,这里我们创建了两个版本的注册方法。一个用于注册遵循ReusableView
协议的子类,另一个用于注册同时遵循ReusableView
和NibLoadableView
协议的子类。这有效地分离了视图控制器的具体注册方法。
另一个很棒的细节是,dequeueReusableCell
方法不再需要任何重用标识符字符串,可以直接使用单元格子类作为其返回值。
现在,单元格注册和使用代码看起来非常简洁和优雅 :) 。
class BookListViewController: UIViewController, UICollectionViewDataSource {
@IBOutlet private weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.register(BookCell.self)
}
func collectionView(collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: BookCell = collectionView.dequeueReusableCell(for: indexPath)
// TODO: 配置单元格
return cell
}
}
总结
如果你正在从 Objective-C 过渡到 Swift,那么值得研究 Swift 强大的新特性,如协议扩展和泛型,以寻找更优雅的实现方法和替代方案。