[Swift] UITableView:从牛刀到小刀 (1)

2016-12-28 21:28:06 +08:00
 banxi1988

对于 iOS 开发来说, UITableView 无疑是一把牛刀. 今天我想介绍的是怎么让这把牛刀像小刀一样好使.

我们想实现的目标是, 对于一些简单的场景,比如一个小的列表,可以像如下这么使用:

 let dataSource = ListDataSource(dataItems:[item1,item2,...])
 tableView.dataSource = dataSource

OK, 我们开始吧.

创建一个实现了 UITableViewDataSource 协议的类.

由于 UITableViewDataSource 协议扩展了 NSObjectProtocl, 所以我们的类最好继承自 NSObject.

class ListDataSource: NSObject, UITableViewDataSource{
  
}

实现 UITableViewDataSource 要求的两个方法:

class ListDataSource: NSObject, UITableViewDataSource{
 
  public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
    return 0
  }
  
  public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
    return UITableViewCell(style: .default, reuseIdentifier: "cell")
  }
  
}

这里就遇到问题了,我们的数据是动态的,不能直接返回 0. 我们的 cell 也不是这样直接一个 UITableViewCell 类啊. 问题是,我们不知道数据是什么,怎么构造 Cell?

引入泛型数组.

于是 ListDataSource 变成了这样:

public class ListDataSource<T>: NSObject, UITableViewDataSource{
  public private(set) var dataItems: [T] = []
 
  public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
    return dataItems.count
  }
  
  public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
    return UITableViewCell(style: .default, reuseIdentifier: "cell")
  }
  
}

引入泛型 UITableViewCell 子类.

解决了上面问题, 同理我们的 UITableViewCell 是不是也可以声明为泛型呢?

于是变成这样:

public class ListDataSource<T,Cell: UITableViewCell>: NSObject, UITableViewDataSource{
  public private(set) var dataItems: [T] = []
 
  public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
    return dataItems.count
  }
  
  public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
    return Cell(style: .default, reuseIdentifier: "cell")
  }
  
}

上面我们对于 Cell 泛型也增加了基本的限制,就是 Cell 是继承自 UITableViewCell 的.

引入 Cell 数据绑定协议

但是这里有一个问题就是, 我们的数据跟 Cell 还没有关联起来呢! 也就是说我们需要 Cell 有关联 数据模型 T 的能力. 上面这个需求体现了两点, 一表明某个能力, 即满足某协议约束. 对于泛型来说,可以增加需要实现的协议作为约束.

于是我们要求,Cell 应该实现如下协议, 可以绑定某一个数据.

public protocol Bindable{
  associatedtype DataItem
  func bind(to item: DataItem)
}

因为我们不知道要绑定的数据类型是什么, 所以我们只能先给一个名字. DataItem. 由实现者确定类型是什么. 也相当是一种泛型.

同时这个 DataItem 应该等于之前的数据泛型 T. 这样,我们得到如下的 ListDataSource

public protocol Bindable{
  associatedtype DataItem
  func bind(to item: DataItem)
}

public class ListDataSource<T,Cell: UITableViewCell>: NSObject, UITableViewDataSource where Cell: Bindable, Cell.DataItem == T{
  public private(set) var dataItems: [T] = []
 
  public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
    return dataItems.count
  }
  
  public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
    let cell =  Cell(style: .default, reuseIdentifier: "cell")
    cell.bind(to: dataItems[indexPath.row])
    return cell
  }
  
}

重点在于 where Cell: Bindable, Cell.DataItem == T 也就是我们上面所说的. 要求 Cell 实现 Bindable 协议.

简化 泛型声明. 去掉泛型 T 声明.

之前由于不确定数据模型的类型,所以我们引入了 T 作为泛型名. 但是后来我们又添加了 Bindable, 其中使用 DataItem 名来标识数据类型. 这里两者相等,看起来重复了. 简化后如下:

public protocol Bindable{
  associatedtype DataItem
  func bind(to item: DataItem)
}

public class ListDataSource<Cell: UITableViewCell>: NSObject, UITableViewDataSource where Cell: Bindable{
  public private(set) var dataItems: [Cell.DataItem] = []
 
  public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
    return dataItems.count
  }
  
  public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
    let cell =  Cell(style: .default, reuseIdentifier: "cell")
    cell.bind(to: dataItems[indexPath.row])
    return cell
  }
  
}

完善一下.

public protocol Bindable{
  associatedtype DataItem
  func bind(to item: DataItem)
}

extension String{
  public static let listCellReuseIdentifier = "listCell"
}

public class ListDataSource<Cell: UITableViewCell>: NSObject, UITableViewDataSource where Cell: Bindable{
  public private(set) var dataItems: [Cell.DataItem] = []
  
  private weak var tableView:UITableView?
  
  public init(dataItems: [Cell.DataItem]){
    self.dataItems = dataItems
  }
  
  public func bind(to tableView: UITableView){
    self.tableView = tableView
    tableView.dataSource = self
    tableView.register(Cell.classForCoder(), forCellReuseIdentifier: .listCellReuseIdentifier)
  }
 
  public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
    return dataItems.count
  }
  
  public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
    let cell =  tableView.dequeueReusableCell(withIdentifier: .listCellReuseIdentifier, for: indexPath) as! Cell
    cell.bind(to: dataItems[indexPath.row])
    return cell
  }
  
}

使用示例

有了上面的代码之后, 我们再显示一个 List 就简单得多了. 例如我们显示一个菜单列表.

// 先定义菜单 Cell, 这是数据模型类型是 String
class MenuCell: UITableViewCell,Bindable{
  typealias DataItem = String
  func bind(to item: String) {
    textLabel?.text = item
  }
}

// 实例化一个 ListDataSource
let dataSource = ListDataSource<MenuCell>(dataItems: [
    "排骨面","红椒面", "青椒面"
  ])

let tableView = UITableView(frame: .zero, style: .plain)
// 将 DataSource 与 UITableView 绑定.
dataSource.bind(to: tableView)

怎么样? 最后实现的结果 ,跟我们的希望应该是差不多的了. 当然这只是一个开始, 为了适用你项目的使用, 你可以在上面的代码的基础上. 添加修改. Hope to see your advice!

3116 次点击
所在节点    iDev
4 条回复
HuangLibo
2016-12-28 21:48:09 +08:00
table view 是牛刀, 那 collection view 岂不是核武器了
FreshOldMan
2016-12-28 21:49:10 +08:00
大哥,每天一篇啊
banxi1988
2016-12-28 21:59:25 +08:00
@HuangLibo 怎么说呢. UICollectionView 从机制或者说潜力上来说, 比 TableView 强大.
但是不如 UITableView 易用. UITableView 在很多使用场景下,基本是开箱即用. 而 UICollectionView 还需要我们自己多加加工.
HuangLibo
2016-12-29 09:22:05 +08:00
@banxi1988 开箱即用,我推荐 IGListKit ,封装了 collection view

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/330855

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX