blog

利用Swift的协议扩展和泛型实现重用

2016-11-11
iOS 技术

作为一名开发者,常见的任务之一是通过子类化自定义单元格来实现 UITableView 或 UICollectionView 的定制。在注册自定义单元格子类方面,UITableViewUICollectionView提供了非常相似的 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协议的子类,另一个用于注册同时遵循ReusableViewNibLoadableView协议的子类。这有效地分离了视图控制器的具体注册方法。

另一个很棒的细节是,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 强大的新特性,如协议扩展和泛型,以寻找更优雅的实现方法和替代方案。