掌握聚合最新动态了解行业最新趋势
API接口,开发服务,免费咨询服务

单向数据流动的函数式 View Controller

View Controller 向来是 MVC (Model-View-View Controller) 中最让人头疼的一环,MVC 架构本身并不复杂,但开发者很容易将大量代码扔到用于协调 View 和 Model 的 Controller 中。你不能说这是一种错误,因为 View Controller 所承担的本来就是胶水代码和业务逻辑的部分。但是,持续这样做必定将导致 Model View Controller 变成 Massive View Controller,代码也就一天天烂下去,直到没人敢碰。

对于采用 MVC 架构的项目来说,其实最大的挑战在于维护 Controller。而想要有良好维护的 Controller,最大的挑战又在于保持良好的测试覆盖。因为往往 View Controller 中会包含很多状态,而且会有不少异步操作和用户触发的事件,所以测试 Controller 从来都不是一件简单的事情。

这一点对于一些类似的其他架构也是一样的。比如 MVVM 或者 VIPER,广义上它们其实都是 MVC,只不过使用 View Model 或者 Presenter 来做 Controller 而已。它们对应的控制器的职责依然是协调 Model 和 View。

在这篇文章里,我会先实现一个很常见的 MVC 架构,然后对状态和状态改变的部分进行抽象及重构,最终得到一个纯函数式的易于测试的 View Controller 类。希望通过这个例子,能够给你在日常维护 View Controller 的工作中带来一些启示或者帮助。

如果你对 React 和 Redux 有所了解的话,文中的一些方法可能你会很熟悉。不过即使你不了解它们,也并不会妨碍你理解本文。我不会去细究概念上的东西,而会从一个大家都所熟知的例子开始进行介绍,所以完全不用担心。你可能需要对 Swift 有一些了解,本文会涉及一些基本的值类型和引用类型的区别,如果你对此不是很明白的话,可以参看一些其他资料,比如我以前写的这篇文章

整个示例项目我放在了 GitHub 上,你可以在各个分支中找到对应的项目源码。

传统 MVC 实现

我们用一个经典的 ToDo 应用作为示例。这个项目可以从网络加载待办事项,我们通过输入文本进行添加,或者点击对应条目进行删除:

注意几个细节:

  1. 打开应用后加载已有待办列表时花费了一些时间,一般来说,我们会从网络请求进行加载,这应该是一个异步操作。在示例项目里,我们不会真的去进行网络请求,而是使用一个本地存储来模拟这个过程。
  2. 标题栏的数字表示当前已有的待办项目,随着待办的增减,这个数字会相应变化。
  3. 可以使用第一个 cell 输入,并用右上角的加号添加一个待办。我们希望待办事项的标题长度至少为三个字符,在不满足长度的时候,添加按钮不可用。

实现这些并没有太大难度,一个刚入门 iOS 的新人也应该能毫无压力搞定。我们先来实现模拟异步获取已有待办的部分。新建一个文件 ToDoStore.swift:

import Foundation

let dummy = [
    "Buy the milk",
    "Take my dog",
    "Rent a car"
]

struct ToDoStore {
    static let shared = ToDoStore()
    func getToDoItems(completionHandler: (([String]) -> Void)?) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            completionHandler?(dummy)
        }
    }
}

为了简明,我们使用简单的 String 来代表一条待办。这里我们等待了两秒后才调用回调,传回一组预先定义的待办事项。

由于整个界面就是一个 Table View,所以我们创建一个 UITableViewController 子类来实现需求。在 TableViewController.swift 中,我们定义一个属性 todos 来存放需要显示在列表中的待办事项,然后在 viewDidLoad 里从 ToDoStore 中进行加载并刷新 tableView:

class TableViewController: UITableViewController {

    var todos: [String] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()

        ToDoStore.shared.getToDoItems { (data) in
            self.todos += data
            self.title = "TODO - (\(self.todos.count))"
            self.tableView.reloadData()
        }
    }
}

当然,我们现在需要提供 UITableViewDataSource 的相关方法。首先,我们的 Table View 有两个 section,一个负责输入新的待办,另一个负责展示现有的条目。为了让代码清晰表意自解释,我选择在 TableViewController 里内嵌一个 Section 枚举:

class TableViewController: UITableViewController {
    enum Section: Int {
        case input = 0, todos, max
    }
    
    //...
}

这样,我们就可以实现 UITableViewDataSource 所需要的方法了:

class TableViewController: UITableViewController {
    override func numberOfSections(in tableView: UITableView) -> Int {
        return Section.max.rawValue
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let section = Section(rawValue: section) else {
            fatalError()
        }
        switch section {
        case .input: return 1
        case .todos: return todos.count
        case .max: fatalError()
        }
    }
    
    override 

原文来自:<\/script>') document.getElementById('contact-fixed').style.display = 'block'; } } else { initQiDianPc() // document.write('