美文网首页Swift编程
【XE2V 项目收获系列】一、YLExtensions:让 UI

【XE2V 项目收获系列】一、YLExtensions:让 UI

作者: YuLeiFuYun | 来源:发表于2020-08-23 19:01 被阅读0次

    文章首发于掘金:https://juejin.im/post/6862954764179603469

    前言

    XE2V 是一个 V2EX 客户端,作为我的第一个项目,我真切的希望能把它写好。这愿望看起来如此普通,但开始之后才发现,写出让自己满意的代码远没有看起来那么简单,以至于直到现在项目还处于未完成的状态。

    由于经验的匮乏及自身的愚钝,许多对一般开发者手到擒来的事情对我来说都成了大问题。不了解的东西太多了,而我应对困难的方法,呃,能避则避。于是,拖延成了常态。但项目总是要完成的,我又不得不在某个时间继续。重新拾起的项目总是左看右看不顺眼,着实面目可憎,心一横,就把项目推倒重来了。于是,时间成了拖延与重写的无尽循环。幸运的是,在项目的一次次重写中,一些问题终究是被解决了,我把它们提取出来做成库,与大家分享。水平所限,定然有诸多不足,请多指教。

    问题的提出

    当一个 UITableView 或 UICollectionView 页面包含多个种类的 cell 时,注册及配置这些 cell 需要写很多重复的代码,譬如,一个 table view 页面包含了四类 cell:ACell、BCell、CCell 和 DCell,在 tableView(_:cellForRowAt:) 方法中,我们可能这样写:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        switch indexPath.section {
        case 0:
            let cell = tableView.dequeueReusableCell(withIdentifier: "ACell", for: indexPath) as! ACell
            cell.configure(model[indexPath.section][indexPath.row])
            return cell
        case 1:
            let cell = tableView.dequeueReusableCell(withIdentifier: "BCell", for: indexPath) as! BCell
            cell.configure(model[indexPath.section][indexPath.row])
            return cell
        case 2:
            let cell = tableView.dequeueReusableCell(withIdentifier: "CCell", for: indexPath) as! CCell
            cell.configure(model[indexPath.section][indexPath.row])
            return cell
        case 3:
            let cell = tableView.dequeueReusableCell(withIdentifier: "DCell", for: indexPath) as! DCell
            cell.configure(model[indexPath.section][indexPath.row])
            return cell
        }
    }
    

    一种模式重复四遍,实在不够优雅。理想中,tableView(_:cellForRowAt:) 方法应该类似这样:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(...)
        cell.configure(data[indexPath.section][indexPath.row])
        return cell
    }
    

    那么,能否找到一种方法实现上面的效果呢?

    简化 tableView(_:cellForRowAt:) 方法

    首先,我们要使得 dequeueReusableCell(…) 方法能够在不同的 Identifier 下返回不同的 cell。如何做到?不透明类型正是用来解决这类问题的。为此,我们给 UITableView 添加一个扩展:

    extension UITableView {
        func dequeueReusableCell(
            for indexPath: IndexPath,
            with identifiers: [String]
        ) -> some UITableViewCell {
            for (index, identifier) in identifiers.enumerated() where index == indexPath.section {
                let cell = dequeueReusableCell(withIdentifier: identifier, for: indexPath)
                return cell
            }
            
            fatalError()
        }
    }
    

    接下来,每类 cell 都要有一个 configure(_:) 方法,这容易完成,扩展一下 UITableViewCell 即可:

    @objc protocol Configurable {
        func configure(_ model: Any?)
    }
    
    extension UITableViewCell: Configurable {
        func configure(_ model: Any?) {  }
    }
    

    于是,tableView(_:cellForRowAt:) 方法中我们就可以这样写:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(for: indexPath, with: ["ACell", "BCell", "CCell", "DCell"])
        cell.configure(model[indexPath.section][indexPath.row])
        return cell
    }
    

    表示 Identifier 的更好方式

    字符串容易出现拼写错误,有没有更好地方式表示 Identifier 呢?一种解决方式是给 cell 添加一个 identifier 属性,这样我们就可以利用 Xcode 的自动补全功能帮助我们避免错误。我们可以这样做:

    protocol ReusableView { }
    
    extension ReusableView {
        static var reuseIdentifier: String {
            return String(describing: self)
        }
    }
    
    extension UITableViewCell: ReusableView { }
    

    然后,在 tableView(_:cellForRowAt:) 方法中使用:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(for: indexPath, with: [ACell.reuseIdentifier, BCell.reuseIdentifier, CCell.reuseIdentifier, DCell.reuseIdentifier])
        cell.configure(model[indexPath.section][indexPath.row])
        return cell
    }
    

    简化 cell 的注册

    另一个出现重复代码的地方是 cell 注册时,比如,当 A、B、C、D 四类 cell 由纯代码方式创建时,我们会这样注册:

    tableView.register(ACell.self, forCellReuseIdentifier: ACell.reuseIdentifier)
    tableView.register(BCell.self, forCellReuseIdentifier: BCell.reuseIdentifier)
    tableView.register(CCell.self, forCellReuseIdentifier: CCell.reuseIdentifier)
    tableView.register(DCell.self, forCellReuseIdentifier: DCell.reuseIdentifier)
    

    能否简化注册过程?

    仔细观察注册方法,其实只需要提供一个 UITableViewCell.Type 类型的参数即可。基于此,我们可以给 UITableView 添加一个这样的扩展:

    extension UITableView {
        func registerCells(with cells: [UITableViewCell.Type]) {
            for cell in cells {
                register(cell, forCellReuseIdentifier: cell.reuseIdentifier)
            }
        }
    }
    

    从而,在注册时只需写一行代码:

    tableView.registerCells(with: [ACell.self, BCell.self, CCell.self, DCell.self])
    

    如果 cell 是用 nib 方式创建的呢?这也简单。我们先扩展一下 UITableViewCell:

    protocol NibView { }
    
    extension NibView where Self: UIView {
        static var nib: UINib {
            return UINib(nibName: String(describing: self), bundle: nil)
        }
    }
    
    extension UITableViewCell: NibView { }
    

    再给 UITableView 添加一个扩展:

    extension UITableView {
        func registerNibs(with cells: [UITableViewCell.Type]) {
            for cell in cells {
                register(cell.nib, forCellReuseIdentifier: cell.reuseIdentifier)
            }
        }
    }
    

    然后我们就可以用类似的方式注册 nib 方式创建的 cell 了。

    如此,问题就都得到了解决。不过,审视一下 dequeueReusableCell(for:with:) 方法和 registerCells(with:) 方法,它们的参数感觉,呃,不大漂亮。有没有更好地表示方式?嗯,我们可以把它们放入 table view 的 model 的属性中,使用时调用一下就行了。

    给 Model 下一个定义

    啊,model!说了这么久,我们还没有考虑过它。什么是 model?你可能会说它是一个提供数据的东西。确实,我们一般都是把 model 当作数据提供者使用。不过,对于 UITableView 的 model,它其实可以承载更多。每个 table view 都有一个 model 和若干种类的 cell,于是,model 和 cell 间可以建立起联系,我们可以把 cell 的类型存入 model 中,在需要时取用。此外,table view 可能会分页,所以 model 最好能有一个 nextPage 的属性。

    有了上面的讨论,我们给 model 下一个定义:

    protocol Pageable {
        var nextPage: Int? { get }
    }
    
    extension Pageable {
        var nextPage: Int? { nil }
    }
    
    protocol ModelType: Pageable {
        static var tCells: [UITableViewCell.Type]? { get }
        static var tNibs: [UITableViewCell.Type]? { get }
        // All cell types, sort by display order
        static var tAll: [UITableViewCell.Type]? { get }
        
        // Store model data in display order
        var data: [[Any]]? { get }
    }
    
    extension ModelType {
        static var tCells: [UITableViewCell.Type]? { nil }
        static var tNibs: [UITableViewCell.Type]? { nil }
        static var tAll: [UITableViewCell.Type]? { nil }
        
        var data: [[Any]]? { nil }
    }
    

    使用

    于是,我们以后使用 UITableView 可以这么做:

    首先,让 model 遵循 ModelType:

    extension SomeModel: Model {
        static var tCells: [UITableViewCell.Type]? {
            [ACell.self, BCell.self]
        }
        
        static var tNibs: [UITableViewCell.Type]? {
            [CCell.self, DCell.self]
        }
        
        static var tAll: [UITableViewCell.Type]? {
            // Sort by display order
            [ACell.self, BCell.self, CCell.self, DCell.self]
        }
        
        var data: [[Any]]? {
            [someA, someB, someC, someD]
        }
    }
    

    接着,在 cell 中实现 configure(_:) 方法:

    class SomeCell: UITableViewCell {
        ...
        // Configure cell
        override func configure(_ model: Any?) {
            ...
        }
    }
    

    最后,在 ViewController 中:

    1. 创建 model 对象
    let someModel = SomeModel(..)
    
    2. 注册 cell
    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        tableView.registerCells(with: SomeModel.tCells!)
        tableView.registerNibs(with: SomeModel.tNibs!)
    }
    
    3. 创建并配置 cell
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(for: indexPath, with: SomeModel.tAll!)
        cell.configure(someModel.data![indexPath.section][indexPath.row])
        return cell
    }
    

    这里对 dequeueReusableCell(for:with:) 方法做了一些修改:

    func dequeueReusableCell(
        for indexPath: IndexPath,
        with cells: [UITableViewCell.Type]
    ) -> some UITableViewCell {
        for (index, cell) in cells.enumerated() where index == indexPath.section {
            let cell = dequeueReusableCell(withIdentifier: cell.reuseIdentifier, for: indexPath)
            return cell
        }
    
        fatalError()
    }
    

    UICollectionView 的解决方案与之类似,就不做介绍了。

    下篇预告

    为了实现刷新操作的统一处理,需要用到状态机,在网络上简单搜寻一番,没有发现让我满意的,于是,我自己写了一个。下篇文章会介绍这个状态机。

    源码地址: YLExtensions

    相关文章

      网友评论

        本文标题:【XE2V 项目收获系列】一、YLExtensions:让 UI

        本文链接:https://www.haomeiwen.com/subject/xnlwjktx.html