你听说过单向数据流吗?但是你不知道它具体如何实现。或者你曾经尝试过一些单向数据流的框架,但是它们都太复杂了?
本教程会教会你在不依赖现有的单向数据流的框架下,采用简单的单向数据流风格来架构出一个基于Realm的应用。
MVC 设计模式一直以来都是 Cocoa 开发的基石(也是一般 UI 开发的基石),但是近年来在网页开发社区流行的其他一些可选项也层出不穷。其中一个就是单向数据流模式,正如 Facebook 开发中使用的 React 和 Flux 模式一样,单向数据流模式解决了复杂应用中双向绑定的问题。
单向数据流模式也开始用于原生移动开发,它可以非常有效地简化原来有着许多任务调度或者严重回调机制的代码,而且可以避免许多因为维护视图控制器中互斥的状态变量而带来的软件缺陷。
作为一个例子,我们准备实现一个简单的时间跟踪应用。它会支持:
这个风格的关键因素:
更多详情,请看 Benjamin Encz’s单向数据流入门
让我们开始吧,但是如果你想直接看最后的代码,你可以看 GitHub 这里 .
创建一个新的 Xcode 的工程,使用 “Single View Application” 模板。确定 “Language” 是 Swift,然后取消 “Use Core Data”。
用你喜欢的依赖解决的方式加入 Realm 和 RealmSwift 框架(关于 CocoaPods, Carthage,和 binary 安装的步骤请看这里)。
加入一个新的 Swift 的文件叫做 ‘Store.swift’,然后创建 Project
和 Activity
的 Realm 对象 - 这些会用来记录应用的状态。
import RealmSwift class Project: Object { dynamic var name: String = "" let activities = List<Activity>() } class Activity: Object { dynamic var startDate: NSDate? dynamic var endDate: NSDate? }
我们也就这个机会给 Project
类加上计算的属性,这会简化我们之后的编码。
extension Project { var elapsedTime: NSTimeInterval { return activities.reduce(0) { time, activity in guard let start = activity.startDate, let end = activity.endDate else { return time } return time + end.timeIntervalSinceDate(start) } } var currentActivity: Activity? { return activities.filter("endDate == nil").first } }
接下来我们要创建存储。好消息是 Realm 已经非常契合单向数据流存储的要求了,我们不需要再写许多模板代码来实现它了。
我们使用内嵌的 Realm 的变化通知机制来触发视图更新 - Realm 的后台线程会自动感知和触发更新通知。
首先,我们给 Realm
扩展些计算属性,它们会返回当前应用的状态 - 在我们应用里,是所有项目的一个列表。
// MARK: Application/View state extension Realm { var projects: Results<Project> { return objects(Project.self) } }
接下来,我们创建一些操作,当然也是通过扩展 Realm
。操作是唯一能够修改 Realm 中数据模型的方法,而且它们不可以有返回值 - 所有对模型的改变都会通过通知的方式广播给视图。这可以保证每次状态更新的时候视图都能一致地重绘,无论更新来自何处。
// MARK: Actions extension Realm { func addProject(name: String) { do { try write { let project = Project() project.name = name add(project) } } catch { print("Add Project action failed: /(error)") } } func deleteProject(project: Project) { do { try write { delete(project.activities) delete(project) } } catch { print("Delete Project action failed: /(error)") } } func startActivity(project: Project, startDate: NSDate) { do { try write { let act = Activity() act.startDate = startDate project.activities.append(act) } } catch { print("Start Activity action failed: /(error)") } } func endActivity(project: Project, endDate: NSDate) { guard let activity = project.currentActivity else { return } do { try write { activity.endDate = endDate } } catch { print("End Activity action failed: /(error)") } } }
在文件末尾,创建一个 Store
的实例。
let store = try! Realm()
现在让我们来实现视图层。打开你的 ViewController.swift
文件然后把它从 UIViewController
重命名为 UITableViewController
的子类。增加一个 projects
的属性并且重载 UITableViewDataSource
方法。我们也会增加一个 UITableViewCell
子类 - 注意在这里无论何时 project
属性变化了,每一个子视图的属性都会被重置;再强调一次,当数据模型变化时,需要确保每一个视图都是一致的更新,这点非常重要。
class ViewController: UITableViewController { let projects = store.projects override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 1 } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return projects.count } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("ProjectCell") as! ProjectCell cell.project = projects[indexPath.row] return cell } } class ProjectCell: UITableViewCell { @IBOutlet var nameLabel: UILabel! @IBOutlet var elapsedTimeLabel: UILabel! @IBOutlet var activityButton: UIButton! var project: Project? { didSet { guard let project = project else { return } nameLabel.text = project.name if project.currentActivity != nil { elapsedTimeLabel.text = "??" activityButton.setTitle("Stop", forState: .Normal) } else { elapsedTimeLabel.text = NSDateComponentsFormatter().stringFromTimeInterval(project.elapsedTime) activityButton.setTitle("Start", forState: .Normal) } } } @IBAction func activityButtonTapped() { guard let project = project else { return } if project.currentActivity == nil { // TODO: start a new activity } else { // TODO: complete the activity } } }
下面我们会把视图控制器注册成为存储的观察者,这样我们就能在状态改变的时候重载视图了。在 ViewController
上实现 Realm 通知如下:
var notificationToken: NotificationToken? override func viewDidLoad() { super.viewDidLoad() updateView() notificationToken = store.addNotificationBlock { [weak self] (_) in self?.updateView() } } func updateView() { tableView.reloadData() }
现在我们用 Interface Builder 把所有的组件联系起来。打开 Main.storyboard
,按如下步骤操作:
View Controller
界面 UITableViewController
视图作为根导航。 Is Initial View Controller
ViewController
ProjectCell
。并且在 Attributes Inspector 上重用标示为 ProjectCell
。 UILabel
和一个 UIButton
,然后设置为 autolayout。在 ProjectCell 上把它们和 nameLabel
, elapsedTimeLabel
和 activityButton
的 outlets 连接起来。当你做完这些,请把 Activity 的按钮的 TouchUpInside 和 activityButtonTapped
操作连接起来。 Root View Controller
更合适的名字。 这样,所有视图控制器的代码都完成了 - 无论何时状态改变,列表都会自动更新了。你现在可以编译和运行你的应用了,虽然没有那么激动人心,因为现在没有任何项目(而且没有办法添加任何项目)!
所以让我们来增加些操作来看看状态更新是如何工作的 - 我们开始增加一个新的项目。因为项目只需要一个名字,最容易的方法就是把 ‘add project’ 放到列表的表头上。
在 storyboard 里, 按照如下步骤创建 ‘add project’ 元素:
newProjectTextField
的 outlet addButtonTapped
。不要忘记把 drop-down 改为 ‘action’! showNewProjectView
。再强调一次,如果 Xcode 不好使用的话,使用 Document Outline。 隐藏 Assistant View 然后转到 ‘ViewController.swift’。 增加显示和隐藏表头的代码,在 addButtonTapped
方法里面调用存储的 addProject
方法。你也需要增加一个 hideNewProjectView()
方法来调用 stateDidUpdate
。
func updateView() { tableView.reloadData() hideNewProjectView() } @IBAction func showNewProjectView(sender: AnyObject) { tableView.tableHeaderView?.frame = CGRect(origin: CGPointZero, size: CGSize(width: view.frame.size.width, height: 44)) tableView.tableHeaderView?.hidden = false tableView.tableHeaderView = tableView.tableHeaderView // tableHeaderView needs to be reassigned to recognize new height newProjectTextField.becomeFirstResponder() } func hideNewProjectView() { tableView.tableHeaderView?.frame = CGRect(origin: CGPointZero, size: CGSize(width: view.frame.size.width, height: 0)) tableView.tableHeaderView?.hidden = true tableView.tableHeaderView = tableView.tableHeaderView newProjectTextField.endEditing(true) newProjectTextField.text = nil } @IBAction func addButtonTapped() { guard let name = newProjectTextField.text else { return } store.addProject(name) }
如果你现在运行你的应用,你应该能增加新的项目了 - 太棒了!当 addProject
被调用的时候,列表自动更新了,尽管我们在 addButtonTapped
里面没有一行 UI 更新的代码 - 应用状态的改变会自动的影响到视图。这就是单向数据流的操作。
剩下的操作就非常直接了 - 我们可以在 ProjectCell.activityButtonTapped
里面增加开始和停止的逻辑:
@IBAction func activityButtonTapped() { guard let project = project else { return } if project.currentActivity == nil { store.startActivity(project, startDate: NSDate()) } else { store.endActivity(project, endDate: NSDate()) } }
然后在 ViewController
里面重载合适的 UITableViewController
的方法来实现 swipe-to-delete:
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { return true } override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { store.deleteProject(projects[indexPath.row]) }
就这么简答!编译然后运行你的超级有用的时间跟踪应用,拍拍自己的脑袋,你已经会实现单向数据流模式了。
在本次教程里面,我们介绍了单向数据流的概念和如何使用内嵌的 Realm 通知功能来实现一个采纳单向数据流模式的应用。特别的,我们采用了这些原则:
我们的应用有很多地方都可以改进,这些都是些非常值得考虑的地方,如果你在你自己的应用里面也采用这些技术的话。
现在你已经是单向数据流的专家了,看看当前已有的框架是非常值得的,看看能不能帮到你自己。请看: