美文网首页程序员
构建Typed,Flexible的UITableView

构建Typed,Flexible的UITableView

作者: Dev端 | 来源:发表于2016-02-26 13:51 被阅读283次

    原文: http://holko.pl/2016/01/05/typed-table-view-controller/


    UITableView是几乎所有iOS开发者的日常. 绝大多数情况下, 我们只用一种数据类型和同一个UITableViewCell和对应的reuse identifier来呈现数据. 当我们想要处理两种或者更多种cell的时候, 问题会变得复杂起来.

    本文将介绍三种解决这一问题的方法. 后两种尝试解决上一种中出现的问题. 第一种方法是我们经常会在Obj-C代码库中看到的解决方案. 第二种方法利用了enumeration来更好地解决这一问题, 第三种方法基于protocol和generics, 属于Swift的最佳实践.

    Basics

    我会借助一个创建多种类cell的例子(在Github上), 如下图:

    效果图

    我喜欢把呈现一个view需要的所有数据封装起来, 叫它view data. 例子的view data中非常简单:

    struct TextCellViewData {
        let title: String 
    }
    struct ImageCellViewData {     
        let image: UIImage
    }  
    

    在实际项目中我们肯定有更多的数据, image属性会变成NSURL类型替代以去除对UIKit的依赖. 我们同时拥有两个用来呈现数据的cell:

    class TextTableViewCell: UITableViewCell { 
        func updateWithViewData(viewData: TextCellViewData) {    
            textLabel?.text = viewData.title 
        }
    }
    class ImageTableViewCell: UITableViewCell { 
        func updateWithViewData(viewData: ImageCellViewData) {
            imageView?.image = viewData.image 
        }
    }
    

    这些ready之后就可以开始处理View Controller了.

    1st Approach: 'Easy'

    我喜欢在解决问题一开始的时候先不要把它复杂化. 先简单地实现功能再说.

    我们用一个叫items的Array作为model层, 由于要包含不同类型的struct, 所以items的类型是[AnyObject]. 我们用registerCells()方法来事先注册cell. 在tableView(_:cellForRowAtIndexPath:)中, 我们检查当前view data的类型来决定用那种cell呈现哪些数据. 以下是具体实现:

    class ViewController: UIViewController {
       
        @IBOutlet weak var tableView: UITableView!
       
        var items: [Any] = [
            TextCellViewData(title: "Foo"),
            ImageCellViewData(image: UIImage(named: "Apple")!),
            ImageCellViewData(image: UIImage(named: "Google")!),
            TextCellViewData(title: "Bar"),
        ]
       
        override func viewDidLoad() {
            super.viewDidLoad()
           
            tableView.dataSource = self
            registerCells()
        }
       
        func registerCells() {
            tableView.registerClass(TextTableViewCell.self, forCellReuseIdentifier: textCellIdentifier)
            tableView.registerClass(ImageTableViewCell.self, forCellReuseIdentifier: imageCellIdentifier)
        }
    }
    
    extension ViewController: UITableViewDataSource {
       
        func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return items.count
        }
       
        func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
            let viewData = items[indexPath.row]
           
            if (viewData is TextCellViewData) {
                let cell = tableView.dequeueReusableCellWithIdentifier(textCellIdentifier) as! TextTableViewCell
                cell.updateWithViewData(viewData as! TextCellViewData)
                return cell
            } else if (viewData is ImageCellViewData) {
                let cell = tableView.dequeueReusableCellWithIdentifier(imageCellIdentifier) as! ImageTableViewCell
                cell.updateWithViewData(viewData as! ImageCellViewData)
                return cell
            }
           
            fatalError()
        }
    }
    

    这样的实现是可行的, 但是我可以总结几点让我觉得不爽的原因:

    1. 我们不能让这个view controller变成可复用的. 如果我们决定添加一个新类型的cell, 比如说一个呈现video的cell,我们必须要在3个地方改代码: 1) 新的reuse identifier, 2)registerCells(), 3)tableView(_:cellForRowAtIndexPath:)

    2. 如果我们决定改变items里面的数据,提供一个新的data type, 我们势必要处理tableView(_:cellForRowAtIndexPath:)fatalError(), 整体的封装性并不好.

    3. 我们明明知道一种cell对应着一种view data, 但在代码中我们并没有体现这种关系.

    2nd Approach: Enumeration

    我们能通过enumeration来解决上述的部分问题

    enum TableViewItem {
        case Text(viewData: TextCellViewData)
        case Image(viewData: ImageCellViewData)
    }
    

    然后我们的items就变成[TableViewItem]:

    var items: [TableViewItem] = [
        .Text(viewData: TextCellViewData(title: "Foo")),
        .Image(viewData: ImageCellViewData(image: UIImage(named: "Apple")!)),
        .Image(viewData: ImageCellViewData(image: UIImage(named: "Google")!)),
        .Text(viewData: TextCellViewData(title: "Bar")),
    ]
    

    接着改一下registerCells():

    func registerCells() {
        for item in items {
            let cellClass: AnyClass
            let identifier: String
            
            switch(item) {
            case .Text(viewData: _):
                cellClass = TextTableViewCell.self
                identifier = textCellIdentifier
            case .Image(viewData: _):
                cellClass = ImageTableViewCell.self
                identifier = imageCellIdentifier
            }
            
            tableView.registerClass(cellClass, forCellReuseIdentifier: identifier)
        }
    }
    

    最后是tableView(_:cellForRowAtIndexPath:):

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let item = items[indexPath.row]
        
        switch(item) {
        case let .Text(viewData: viewData):
            let cell = tableView.dequeueReusableCellWithIdentifier(textCellIdentifier) as! TextTableViewCell
            cell.updateWithViewData(viewData)
            return cell
        case let .Image(viewData: viewData):
            let cell = tableView.dequeueReusableCellWithIdentifier(imageCellIdentifier) as! ImageTableViewCell
            cell.updateWithViewData(viewData)
            return cell
        }
    }
    

    这样操作的好处:

    1. 对于一个viewController实例来说, 数据的入口脱离了UITableViewCell, 仅仅只有view data了.

    2. 我们用switch语句代替了if语句, 避免了fatalError()

    我们还可以在为这种实现更进一步, 比如说简化dequeing:

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let item = items[indexPath.row]
        
        switch(item) {
        case let .Text(viewData: viewData):
            return tableView.dequeueCellWithViewData(viewData) as TextTableViewCell
        case let .Image(viewData: viewData):
            return tableView.dequeueCellWithViewData(viewData) as ImageTableViewCell
        }
    }
    

    这样操作有一个坏处就是, 我们要在UITableView的各个协议方法以及自己定义的中介方法中不停地重复书写switch语句. 虽然现在我们只要写两个, 但可以想象具体项目里我们要书写的数目之庞大. 比如说, AutoLayout成为性能瓶颈, 我们决定用手动layout整个tableView内容的时候, 我们就得用在tableView(_:heightForRowAtIndexPath:)再写一次Switch了.

    虽说我对此还是可以接受的, 但是一想到将来要重复写的这些代码, 我就觉得头疼.

    3rd (Final) Approach: Protocols and Generics

    让我们暂时先忘掉之前的第一种和第二种实现方法, 从头开始.

    Updatable

    我们用view data来更新cell的数据, 所以我们可以为cell引入Updatable协议, 协议中囊括更新所需的的ViewData.

    protocol Updatable: class {
        typealias ViewData
        
        func updateWithViewData(viewData: ViewData)
    }
    

    让cell纷纷遵循它:

    extension TextTableViewCell: Updatable {
        typealias ViewData = TextCellViewData
    }
    
    extension ImageTableViewCell: Updatable {
        typealias ViewData = ImageCellViewData
    }
    

    在之前两个方法实现的时候, 我们不难发现对于每一个在items中的view data而言, 我们需要它:

    • 知道它所对应的cell的类型
    • 知道它所对应的reuse identifier
    • 包含更新cell所需要的数据(图片,文字)

    CellConfigurator

    因此, 让我们把view data再wrap一层, 让新的struct能够包含所有Cell所需要的数据, 我们姑且叫它CellConfigurator:

    struct CellConfigurator<Cell where Cell: Updatable, Cell: UITableViewCell> {
        
        let viewData: Cell.ViewData
        let reuseIdentifier: String = NSStringFromClass(Cell)
        let cellClass: AnyClass = Cell.self
        
    ...
    

    这是一个generic的struct, 包含一个Cell参数. Cell必须遵循Updatable, 并且是一个UITableViewCell

    CellConfigurator有3个属性: viewData,reuseIdentifiercellClass. viewData的类型依赖于Cell的类型, 它是唯一没有默认值的属性. 另外两个属性都会根据Cell的类型拿到对应的默认值.

    ...
    // further part of CellConfigurator
    
    func updateCell(cell: UITableViewCell) {
        if let cell = cell as? Cell {
            cell.updateWithViewData(viewData)
        }
    }
    }
    

    最后,我们把updateCell()放入UITableViewCell之中. 这是一段非常精简的代码.

    然后,我们在items里放入CellConfigurator的实例:

    let items = [
        CellConfigurator<TextTableViewCell>(viewData: TextCellViewData(title: "Foo")),
        CellConfigurator<ImageTableViewCell>(viewData: ImageCellViewData(image: UIImage(named: "Apple")!)),
        CellConfigurator<ImageTableViewCell>(viewData: ImageCellViewData(image: UIImage(named: "Google")!)),
        CellConfigurator<TextTableViewCell>(viewData: TextCellViewData(title: "Bar")),
    ]
    

    但是这时候编译器会报一个错:

    Type of expression is ambiguous without more context

    这是因为CellConfigurator是generic的, 但是Swift的Array是homogeneous(同质的)的. 所以我们把CellConfigurator<TextTableViewCell>CellConfigurator<ImageTableViewCell>放到一个Array里, 就会报错.

    仔细想想, Cell这个参数只有在定义viewData的时候用到了. 我们可以通过添加一个non-generic的protocol来把Cell这个参数隐藏掉.

    protocol CellConfiguratorType {
        var reuseIdentifier: String { get }
        var cellClass: AnyClass { get }
        
        func updateCell(cell: UITableViewCell)
    }
    

    CellConfigurator遵循它:

    extension CellConfigurator: CellConfiguratorType {
    }
    

    然后我们把items的类型改为:

    let items: [CellConfiguratorType]
    

    bingo, 编译器报错消失.

    View Controller

    我们现在可以更新我们的ViewController了, registerCells()变得非常精简:

    func registerCells() {
        for cellConfigurator in items {
            tableView.registerClass(cellConfigurator.cellClass, forCellReuseIdentifier: cellConfigurator.reuseIdentifier)
        }
    }
    

    tableView(_:cellForRowAtIndexPath:)也变得简单很多:

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cellConfigurator = items[indexPath.row]
        let cell = tableView.dequeueReusableCellWithIdentifier(cellConfigurator.reuseIdentifier, forIndexPath: indexPath)
        cellConfigurator.updateCell(cell)
        return cell
    }
    

    还有一些别的让我们的view controller变得reusable的步骤, 比如说允许items的添加删除. 我们在这里就不一一列举了, 具体的实现在被封装成了framework在这儿: GitHub: ConfigurableTableViewController.

    Conclusion

    让我们最后来总结一下第三种方法解决了前两种方法的哪些痛点:

    1. 我们在加入新类型cell的时候,不再需要修改view controller.
    2. view controller是type safe的, 如果我们提供了不满足需求的view data, 编译器会报错
    3. 我们不再需要重复写switch语句

    由此看来, 第三种方法解决了我们所有的问题. 这印证了一句话:
    A better solution is often just around the corner.

    Thanks to Maciej Konieczny and Kamil Kołodziejczyk for reading drafts of this.

    中文翻译: Dev端

    相关文章

      网友评论

        本文标题:构建Typed,Flexible的UITableView

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