UITableView是一个在app开发中常用的UI控件,我们可以把它定制成任何满足我们业务需求的列表,在动手实现一个真实的UITableView之前,我们通过这段视频模拟一个在iOS编程中经常会用到的概念: Delegate。
首先,我们定义两个类。一个表示列表,一个表示列表里的每一行单元格:
class ListCell {
}
class ListTable {
}
接下来,假设我们要通过一个Controller来管理ListTable的创建和各种事件:
class ListTableController {
var listTable: ListTable!
init() {
self.listTable = ListTable()
}
}
然后,我们给ListCell和ListTable各添加一个方法draw(),用于表示单元格和列表的渲染。
class ListCell {
func draw() { print("I'm a list cell.") }
}
class ListTable {
func draw() {
// 1\. how many rows?
// 2\. how to render each row?
}
}
在ListCell里,我们简单打印一个字符串"I'm a list cell"表示列表每一行的内容。当我们要绘制一个列表时,接下来我们要解决两个问题:
- 我们要绘制多少行呢?
- 我们的每一行要绘制成什么样子呢?
为了给ListTable提供这个信息,我们有几种不同的做法:
第一种就是直接把这个信息写成ListTable的属性:
class ListTable {
var rows: Int = 0
var cell: ListCell?
func draw() {
// 1\. how many rows?
// 2\. how to render each row?
}
}
这样做看似自然,但是当ListTable的属性越来越多的时候,ListTable的各种方法之间的关系就很容易变的复杂,而方法的实现也很容易变的臃肿,因此iOS并没有采用这种做法,而是把"提供这些属性信息"的任务变成了一个protocol。
protocol ListTableDataSource: class {
func numberOfRows(listTable: ListTable) -> Int
func cellForRowAtIndex(listTable: ListTable, index: Int) -> ListCell
}
ListTableDataSource定义了两个方法,它们的第一个参数,表示要设置的ListTable对象:
- numberOfRows返回一个整数,表示要绘制多少行:
- cellForRowAtIndex返回一个ListCell对象,表示"第index行"绘制成什么样子;
有了这个protocol之后,我们删掉ListTable中原来的属性,添加一个新的属性dataSource,表示"任意一个遵从ListTableDataSource"的类型:
class ListTable {
var dataSource: ListTableDataSource?
func draw() {
// 1\. how many rows?
// 2\. how to render each row?
}
}
有了提供列表属性的方法之后,我们就可以动手实现ListTable的draw()方法了:
class ListTable {
var dataSource: ListTableDataSource?
func draw() {
if (self.dataSource != nil) {
for r in 1...self.dataSource!.numberOfRows(self) {
let listCell = self.dataSource!.cellForRowAtIndex(self, index: r)
listCell.draw()
}
}
}
}
实现的过程很简单,我们只是循环了dataSource指定的行数,然后在每一行,绘制了cellForRowAtIndex返回的对象。然后,我们给ListTableController添加一个新的方法,用于设置ListTable的dataSource属性。
class ListTableController {
var listTable: ListTable!
init() {
self.listTable = ListTable()
}
func listWillDisplay() {
self.listTable.dataSource = self
}
}
这里由于我们把dataSource设置成了ListTableController,因此,我们需要让ListTableController遵从ListTableDataSource protocol,并实现其约定的方法。
class ListTableController: ListTableDataSource {
var listTable: ListTable!
init() {
self.listTable = ListTable()
}
func listWillDisplay() {
self.listTable.dataSource = self
}
func numberOfRows(listTable: ListTable) -> Int {
return 10
}
func cellForRowAtIndex(listTable: ListTable, index: Int) -> ListCell {
return ListCell()
}
}
接下来,我们就可以像下面这样绘制一个列表了:
首先,定义一个Controller:
var listTableController = ListTableController()
其次,初始化ListTable的dataSource:
listTableController.listWillDisplay()
最后,绘制ListTable:
listTableController.listTable.draw()
这时,按 Command + R,编译执行,我们就可以在控制台看到ListTable的"绘制"过程了:
模拟绘制不同的列表行
除了让ListTable的每一行都一样之外,我们还可以分别定制每一行的内容。因为有了dataSource,这并不是什么难事儿,首先我们从ListCell派生一个新类,表示一个新的列表行:
class CustomListCell: ListCell {
override func draw() { print("I'm a custom list cell.") }
}
同样,我们只是简单的输出一个字符串表示绘制的过程。
然后,在ListTableController的cellForRowAtIndex实现里,我们只要根据不同的index,返回不同的ListCell对象就可以了:
class ListTableController: ListTableDataSource {
// Omit for simplicity...
func cellForRowAtIndex(listTable: ListTable, index: Int) -> ListCell {
if (index % 2 == 0) {
return CustomListCell()
}
else {
return ListCell()
}
}
}
这样,所有的偶数行都会是CustomListCell:
模拟ListTable的事件处理
除了把"提供ListTable属性"这件事情外包出去之外,我们还可以用类似的方式,把ListTable可能发生的各种事件外包出去。我们定义一个新的protocol:
protocol ListTableDelegate: class { }
接下来,假设我们要处理列表某一行被点击的事件,我们可以给ListTableDelegate添加一个新的约束:
protocol ListTableDelegate: class {
func didSelectRowAtIndex(listTable: ListTable, index: Int)
}
然后,我们给ListTable添加一个新的属性:
class ListTable {
// Omit for simplicity...
var delegate: ListTableDelegate?
}
并且,添加一个模拟触发某一行被点击的方法:
class ListTable {
var delegate: ListTableDelegate?
func triggerCellSelection(index: Int) {
if (self.delegate != nil) { // Ignore the out of index case
self.delegate.didSelectRowAtIndex(self, index: index)
}
}
}
这里,我们只是简单的把被选中的行数,传递给了delgate,其它的事情,就全交由它处理了。
接下来,为了让ListTableController可以处理ListTable的选中事件,我们要让它遵从ListTableDelegate:
class ListTableController: ListTableDataSource, ListTableDelegate { }
然后,实现ListTableDelegate约定的方法。这里,我们只是简单向控制台打印一个字符串:
class ListTableController: ListTableDataSource, ListTableDelegate {
var listTable: ListTable!
// Omit for simplicity...
func didSelectRowAtIndex(listTable: ListTable, index: Int) {
let cell = self.cellForRowAtIndex(self.listTable, index:index)
print("\(cell.dynamicType) \(index) is selected.")
}
}
然后,在listWillDisplay()里,把listTable的delegate设置成自己:
class ListTableController: ListTableDataSource, ListTableDelegate {
// Omit for simplicity...
func listWillDisplay() {
self.listTable.dataSource = self
self.listTable.delegate = self
}
}
最后,我们模拟一个第二行被选中的事件:
listTableController.listTable.triggerCellSelection(2)
这时,按 Command + R,就可以在控制台看到"某行被选中"这个事件被成功处理了。
解决Delegate模式下的循环引用
在我们上面的代码里,隐藏着一个会引起循环引用的问题,这是在使用Delegate模式的时候,一定要注意的事情:
首先,我们在ListTableController的init(),创建了一个指向ListTable对象的strong reference:
然后,我们的listWillDisplay()中,创建了两个指回ListViewController的strong reference:
这样ListTable和ListTableController之间,就形成了一个循环引用,使得他们彼此都不能正确被销毁,而在之前的视频中,我们提到过,解决这种问题的方法很简单,我们让dataSource和delegate为weak reference就可以了:
class ListTable {
weak var dataSource: ListTableDataSource?
weak var delegate: ListTableDelegate?
// omit for simplicity
}