轻量 View Controller 实践

文章目录
  1. 1. 轻量化的 View Controller
    1. 1.1. 分离 Data Source
    2. 1.2. 实现 Model
    3. 1.3. 单元测试
    4. 1.4. 处理 View 与 View Controller 关系
    5. 1.5. TableView 内多种 Section 的处理

MVC 是 iOS 应用开发时常用的一种架构,使用这种架构的目的之一便是为了写出后期可维护的代码。去年我使用 Swift 1.2 编写了我的第一个 iOS 应用,我称它为 Percolator,其中包含了我对 MVC 初步实践。由于自己初学应用开发与设计,在后期维护时发现程序整体结构过于耦合,有些地方违反了 MVC 的设计要求,并且滥用单例,也没有考虑到循环引用等等问题。今年夏天 Xcode8 beta + Swift3.0 beta 放出,利用假期这段时间完成了 Percolator 的重构,这篇手记记录了设计良好架构的一些实践,由于水平有限,若有不正确的地方还望批评指正。

轻量化的 View Controller

分离 Data Source

著名博客 objc.io 的第一篇期刊中介绍了如何对 View Controller 进行瘦身,提出了将 UITableViewUICollectionView 的 Data Source 部分从 View Controller 中单独分离出去的实践,并且提供了示例代码。不过示例代码非常简单,需要进一步阐述清楚这个概念。

在 Percolator 进行重构时我反复测试,最终得出了一种遵循此概念且易于实现和扩展的结构:

分离 Data Source

箭尾对象拥有箭头所指对象,实线为强引用,虚线为弱引用,Controller 强引用 DataSource 与 Model 实例,这里没有画出。在实现时,我们将 Model 实现一个自定 DataProvider 协议,代码如下:

1
2
3
4
5
6
7
8
9
10
11
protocol DataProvider: class {

associatedtype ItemType

func numberOfSections() -> Int
func numberOfItems(in section: Int) -> Int

func item(at indexPath: IndexPath) -> ItemType
func identifier(at indexPath: IndexPath) -> String

}

之后我们创建一个支援泛型的类 TableViewDataSource(一个包含成员 model 的自定类 DataSource 的子类),并利用 DataProvider 所提供的方法实现 UITableViewDataSource 协议,其头部声明如下:

1
2
3
class TableViewDataSource<Model: DataProvider, Cell: ConfigurableCell where Model.ItemType == Cell.ItemType, Cell: UITableViewCell>: DataSource<Model>, UITableViewDataSource {

}

此时我们将 DataSource 与其管理的数据进行了分离。我们从 DataProvider 内获得数据的数量,调用 identifier(at:) 方法获得重用标志符,调用 item(at:) 获得初始化 Cell 所需数据。Cell 是一个实现了自定协议 ConfigurableCell 的 UITableViewCell 子类,协议规定了一个 configure(with item: ItemType) 方法。这里利用到了 Swift 语言的关联类型的特性,即协议中的具体类型在实现该协议时给出。

在实例化 TableViewDataSource 时,我们将一个实现了 DataProvider 协议的 Model 传入,赋给成员变量 model,并且明确泛型 Cell 的具体类型,Model 的关联类型与 ConfigurableCell 的关联类型由 TableViewDataSource 中泛型声明的 where 子句来判断并保持一致,若其中的一方关联类型变动,代码将无法通过编译,确保类型安全。

实现 Model

将数据操作逻辑放在实现了 DataProvider 协议的 Model 类中,该类只暴露相关方法给 View Controller,从而实现 Model 与 View 的分离,达成 MVC 的结构。

需要注意的是,Model 类只保留一个 tableView 实例的弱引用,确保 View Controller 之与 Model 之间不会出现循环引用的情况。同时要确保对数据进行增改删之后尽快调用 tableView.reaload() 或相关方法刷新视图,防止 UI 与数据不一致的情况。

涉及从网络获取数据时,将网络调用相关方法打包为一个类,且使用单例。这样做的好处在于可以方便的进行缓存设置,当网络调用涉及到用户验证和授权时,可以在应用初始化过程进行登录检查并设置好网络层,进行网络请求时检查类中相关变量的状态即可调整方法的行为。

在进行方法回调时,可将处理数据的过程放在后台线程并行处理,但将处理完毕后需将回调返回给主线程,否在在 Model 中进行视图刷新时不能确保数据被及时刷新,造成应用崩溃。在回调时,可利用 Swift 函数式编程的特性,将错误打包逐层传递,这里不展开介绍,可参考此链接

单元测试

将 Model 与 DataSource 声明为 View Controller 的强引用类型的实例变量,在 viewDidLoad() 方法中对这两个变量进行初始化,再将 tableView.dataSource 设置为分离好的 DataSource 就完成了 View Controller 的设置。同样的,在单元测试中创建简单的数据就可复用相关代码,之后在 Storyboard 内创建 UITableViewControllerUITableViewCell 的 Mock 对象,更新数据并检查相应的 Cell 内的值是否被正确设置,以确保相关代码的正确性,减轻后期维护时的负担。

处理 View 与 View Controller 关系

实现了 DataSource 的分离后,虽然类间的耦合度下降,但同时也会出现一些问题。例如:如何将 View(UITableViewCell)内的 delegate 设置为 View Controller,以处理 Cell 内的 button 被按下之类的事件(View 不是 Controller,此类事件不应由 View 处理)。由于 DataSource(Model 同)不拥有 View Controller 的引用,因此这里使用 View Controller 的 tableView(_:, willDisplay:, forRowAt:) 方法来检查 Cell 的类型并设置 delegate 的方法,同样的,该 delegate 应是 View 指向 View Controller 的弱引用,防止循环引用造成内存泄漏。

TableView 内多种 Section 的处理

由于在 DataSource 中规范了 Model 与 Cell 的关联值类型,因此处理一个含有多种 section 的 TableView 变得几乎不可能。好在 Swift 提供了 enum 的关联值类型,将关联值类型设置为包含不同关联类型的自定 enum,将不同的 Cell 继承于一个关联类型匹配的父类,然后重载其 configure(with:) 方法并取出相应关联值。

以上简要说明了轻量 UITableViewController 的构建,类似的方式实现 CollectionViewDataSource 即可完成轻量 UICollectionViewContoller 的构建。