模拟UITableView使用模式

230 阅读5分钟

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"表示列表每一行的内容。当我们要绘制一个列表时,接下来我们要解决两个问题:

  1. 我们要绘制多少行呢?
  2. 我们的每一行要绘制成什么样子呢?

为了给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的"绘制"过程了:

image


模拟绘制不同的列表行

除了让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:

image


模拟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,就可以在控制台看到"某行被选中"这个事件被成功处理了。

image


解决Delegate模式下的循环引用

在我们上面的代码里,隐藏着一个会引起循环引用的问题,这是在使用Delegate模式的时候,一定要注意的事情:

首先,我们在ListTableController的init(),创建了一个指向ListTable对象的strong reference:

image

然后,我们的listWillDisplay()中,创建了两个指回ListViewController的strong reference:

image


这样ListTable和ListTableController之间,就形成了一个循环引用,使得他们彼此都不能正确被销毁,而在之前的视频中,我们提到过,解决这种问题的方法很简单,我们让dataSource和delegate为weak reference就可以了:


class ListTable {
    weak var dataSource: ListTableDataSource?
    weak var delegate: ListTableDelegate?

    // omit for simplicity
}