美文网首页程序员
构建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