Swift POP 应用 (一) UITableViewCell 的注册与获取

2018/9/17 posted in  iOS

最近在学习 Swift 中协议的使用。 在上一篇聊到 Swift 中 POP 的优势之后, 由于参加了 SwiftGG 举办的 Swift 开发者大会, 听了 Anddy Hope 在项目 neno 上介绍新的代码组织方式。 但是主题之外,看到了一起其它不错的地方。

想到以前看到没故事的卓同学文章中讲到有关协议的应用场景,在联想到当前项目中的使用,因此把 neno 项目中的一些代码拿出来聊一聊.

最传统的使用方式

通常,我们在使用 UITableViewCell 或者 UICollectionViewCell 时, 要将对应的 cell 注册在 UITableView 或者 UICollectionView 中, 如下方代码所示

let identifier: String = "DemoTableViewCell"
//注册
tableView.register(DemoTableViewCell.self, forCellReuseIdentifier: identifier)

//获取
let cell = tableView.dequeueReusableCell(withIdentifier:identifier, for: indexPath) as! DemoTableViewCell

这是使用上最基本的流程, 如果对于获取 cell 时使用的强制类型转换抱有担心, 可以只用 as?

进一步的使用方式

在具体的 ViewController 设置 cell 对应的标识符identifier是非常不好的。因为当前 ViewController 可能有多个 cell,一一指定是很杂的。 另外一个问题是, 当前的 UITableViewCell 可能在多个 ViewController 中进行复用, 如果没有 ViewController 中都进行这个的标识, 也是非常费事费力的。
因此,进一步的实现是, 我们把每个 cell 对应的标识符设置在 cell 的声明文件中

//声明
class DemoTableViewCell: UITableViewCell {
    static let identifier: String = "DemoTableViewCell"
}

//注册
tableView.register(DemoTableViewCell.self, forCellReuseIdentifier: DemoTableViewCell)

//获取
let cell = tableView.dequeueReusableCell(withIdentifier:DemoTableViewCell.identifier, for: indexPath) as! DemoTableViewCell

如此,则避免了 identifier 的多处声明。 通常在 OC 的代码中,类似的方式就到此为止了。
但是在 tableView 注册 cell 以及 获取 cell 的过程中,每次方法的调用都使用了两次 cell 这个类,这样做是有些冗余的, 毕竟 Swift 中,泛型与协议也是非常强大的。类似的场景,我们可以传入一个类, 而获取类似的效果。

在场景中引入 Protocol

我们可以默认实现一个协议,协议要求制定一个计算属性,默认返回类名

protocol Reusable: class {
 static let identifier: String {get} 
}

extension Reusable {
    static let identifier: String {
        return String(describing: self)
    }
}

这样,只要对应的类实现协议Reusable,则默认会继承协议的默认方法。

extension UITableViewCell: Reusable {}
extension UICollectionViewCell: Reusable {}

这种情况下, 对于所有的UITableViewCell, UICollectionCell的相应子类则会默认拥有identifier这个属性,而不需要每次手动进行制定。确实快捷了许多。

在场景中引入泛型

上面协议的引入,解决了identifier 在每次 UITableViewCell 进行声明时手动创建的问题,那么对于每次 cell 的注册获取时,如何把两次类型的手动输入进行缩减呢? 这里就需要用到泛型了。
不得不说,泛型是一个非常强大的功能。它一定程度上大大缩减了代码的书写复杂度。

extension UITableView {
    //注册
    func register<T: UITableViewCell>(_: T.Type){
        register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
    }
    
    
    //获取
     func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T {
        if let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T {
            return cell
        }
        else {
            fatalError("The dequeueReusableCell \(String(describing: T.self)) couldn't be loaded.")
        }
    }
}

用过实现 UITableView 的扩展,新增两个对于regisiterdequeueReusableCell进行增强的函数,引入泛型, 最终实现,输入一次参数的结果。 下面是具体的使用:

//注册
tableView.register(DemoTableViewCell.self)

//获取
let cell: DemoTableViewCell = tableView.dequeueReusableCell(indexPath: indexPath) // 用过泛型,直接可以输出对应的类型

最终得到的效果是,tableView 中registerdequeueReusableCell方法,只需要输入需要的类型,就能够得到想要得到的结果,不仅在书写上大大的提速,而且在整洁性上有了极大的提升。
尤其是在泛型的使用过程中,let cell: DemoTableViewCell = tableView.dequeueReusableCell(indexPath: indexPath) 通过在方法指定返回类型,进一步推断方法内部要转的方法,这也是编译器"强大"的一个体现。

demo写到这里,已经非常的简洁了,但是还有一个问题是,通常注册 UITableViewCell 有两种途径: 通过 nib 或者 class。 上述讲到的,都是通过注册 class 来进行创建的, 那么nib 这种方式是如何创建的呢?

如何使用nib进行创建

对于nib的创建与注册,我们也可是实现类似protocol 默认方法 的方式来进行实现。
首先我们要做的是, 创建 nib 相关协议的实现。 NibLoadable默认实现了,通过类名来获取 nib实例的方法。

protocol NibLoadable {
 static var nib: UINib { get }
}

extension NibLoadable {
    
    static var nib: UINib {
        return UINib(nibName: String(describing: self), bundle: Bundle(for: self))
    }
}

当然,并非所有的 UIView 或者 UITableViewCell 都是可以通过nib 进行获取的,因此我们要有选择的对其进行实现。 这里就需要开发者手动指定是否要实现NibLoadable 这个协议了。

class DemoNibTableViewCell: UITableViewCell, NibLoadable {
  
    override func awakeFromNib() {
        super.awakeFromNib()
    }
}

对于需要通过.xib进行创建的cell, 我们需要手动继承NibLoadable提供的默认实现。

extension UITableView {
 func register<T: UITableViewCell>(_: T.Type) where T: NibLoadable {
        register(T.nib, forCellReuseIdentifier: T.reuseIdentifier)
    }
}

需要注意的是,与上文中实现的register方法不同,此处实现的方法通过where进行了类型限定
要求传入的参数要满足的两个条件 1. 是 UITableViewCell的子类 2. 实现 NibLoadable 协议。

现在我们把两个方法放在一起进行一下对比:

 func register<T: UITableViewCell>(_: T.Type) where T: NibLoadable {
        register(T.nib, forCellReuseIdentifier: T.reuseIdentifier)
    }
    
func register<T: UITableViewCell>(_: T.Type){
        register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
   }

两者看起来,在方法调用时几乎看不出区别,但是在具体调用时却会走不同的方法。 唯一的区别就是 nibLoadable 的方法要求的参数更加严格一点, 我们也可以看出, 第一个方法的参数是第二个方法参数的"子集".

那么我们来看一下具体的调用场景

tableView.register(DemoTableViewCell.self) //通过 UITableViewCell.class 进行创建
tableView.register(DemoNibTableViewCell.self) //通过nib 进行创建。

通过上面的例子我们可以看到,在做好了底层的工作之后,tableView 进行 cell 注册时, 开发者几乎不需要区分具体的 cell 是通过 nib 创建还是通过 class 创建。 这也是一个生产力! 唯一需要注意的就是,在实现UITableViewCell时, 要有针对性的添加 NibLoadable 这个协议, 获取其默认实现。 这相对于传统的方法已经是一个成本低得多的方案。

总结

本文中拿出 Anddy Hope 在demo中的例子进行了一些说明。虽然作者在项目中也有类似的实现,但是与之相比,还是稍有瑕疵。
在该项目的代码中,除了 UITableViewCell 的一些基本的使用, 对于 UITableViewHeaderFooterView、 UICollectionViewCell 同样进行扩展使用。

总结来说, Swift 中的协议真的是一个非常强力的工具。 熟练的进行掌握, 抽取一些共有的属性、共有方法,有选择的进行协议的实现。
多用组合,少用继承,这是一个非常有效的准则。