Reactive Cocoa 3.0 在 MVVM 中的应用

628 查看

这个是我最后一篇关于 ReactiveCocoa 3.0 (简称 RAC3)的文章,主要介绍了在 MVVM 实践中更多复杂的 RAC 3.0 的用法。

(译者注:前两篇为《ReactiveCocoa 3.0 初见》和 《ReactiveCocoa 3.0 初见(2)》)

ReactiveCocoa 3.0 当前还处于测试阶段,截止本文发表当天,已经有 4 个 beta 版。相较于 Objective-C 版本,3.0 版本带来了全新的 Swift API。尽管函数响应式编程的核心理念保持不变,但是 Swfit API 相比于之前的版本却有很大的不同。它使用了泛型,自定义操作符和柯里化函数,实现效果相当不错。在我前面的几个版本中,我探讨了一般性的 Signal 类,它提供了强类型的信号管道模型,还有一篇关于 SignalProducer 的类接口,SignalProducer 为具有附带作用的信号提供了一种更清晰的表达方式。

自从发布这些文章,有很多人请求我演示更多复杂的 RAC3 代码例子,于是我就写下这篇文章。

一个快速的 MVVM 回顾

ReactiveCocoa 本身并不是一个 MVVM 的框架,然而,它让那些使用 MVVM 这种流行的 UI 架构的 app 变得更加容易构建。

这种模式的核心是 ViewModel。它是一个特殊类型的 model,反映 app 的 UI 状态。它包含每个 UI 的详细状态,比如当前 textField 的文本内容,或者按钮的 enable 状态。它也提供了视图可以进行的响应操作,比如响应按钮的点击或者手势识别的响应操作。

从 iOS 开发层面来具体看 MVVM 模式,View 由 ViewController 和对应的 UI 组成(无论是 nib,storyboard 还是代码生成)

使用 MVVM 会使得 View 层变得简单,只需进行反映当前的 UI 状态和其他一点点工作。

ReactiveCocoa 在 MVVM 应用中扮演者特殊的角色,提供一个简单的途径来同步视图和它所关联的 ViewModel。

在 REACTIVECOCOA 2.0 中进行 MVVM

在 RAC2,将 ViewModel 上的属性绑定到视图上通常需要使用一堆宏:

上述代码通过宏将 loadingIndicator 的 hidden 的属性绑定到 ViewModel 的 executing signal 上。另一个有用的宏是 RACObserve,可以从属性中生成一个信号,作用相当于一个对 KVO 的高效封装。

(如果你之前没有在 MVVM 中使用 ReactiveCocoa,你可以看看我之前的文章 tutorial on Ray Wenderlich’s site

不幸的是,RAC2 这些宏使用起来有些笨拙和累赘。所有基于宏的 API 都是如此,不单单只是 RAC 这些。

RAC3 不再使用宏和 KVO,而是单纯 Swift 风格的实现。

RAC3 属性

几个月前,我写了一篇 KVO and a few KVO-alternatives with Swift,主要说的是缺少强类型,过度依赖 NSObject 和过于笨拙的语法,意味着 KVO 并不适合 swift 的世界。

在 RAC3,属性(至少你要监听的属性)使用 MutableProperty 类型表示:

这个实例化了一个 name 属性,类型是 String,初始值是空字符串。注意 name 这个属性是个常量(用的是 let),尽管它代表着一个可变的属性。

可变属性有一个很简单的 API,一个 value 属性和一个 put 方法,允许你 get 或 set 操作当前的值:

他们还暴露了一个类型为SignalProducerproducer 属性,允许你观察属性的变化:

…每一步都很清晰,并且是强类型。

在 MVVM 模式,配置一个 ViewModel 通常都会绑定一个属性。换句话说,就是确保视图的变量属性与对应的 ViewModel 属性保持同步。

出于这个目的,RAC3 有个明确的操作符:

上述代码,viewModel 的 queryExecutionTime 绑定了 textField 的 rac_text 属性。

注意: 当前 RAC3 不支持双向绑定

一个 MVVM RAC3 的例子

作为约定,这篇博客包括了一个更深入的 RAC3 的例子。一个 twitter 的搜索 demo:

(对的,我的所有代码都与 Twitter 和 Flicker API 有关,如果你在天朝,自重)

这个 app 会根据给定的 text 来搜索 tweet,搜索会在用户输入后自动执行。

TwitterSearchViewModel 有如下这些属性:

这些属性就是所有 View 反映视图 UI 状态需要知道的属性,并且允许通过 RAC3 绑定响应更新。tweet 的 tableView 通过 tweets 可变属性被‘返回’,它包含一个数组,里面是 ViewModel 实例,每个都对应一个独立的 cell。

TwitterSearchService 类是一个基于 RAC3 Twitter API 的封装,用 signal producers 代表了网络请求。

这个应用的核心代码如下:

这个请求访问了用户 twitter 账户,接着管道将操作传给 searchText.producer,也就是它观察了它自己的 searchText 属性。你将会注意到 producer 并没有直接使用,而是先调用 map 方法 searchText.producer |> mapError。这在 RAC3 中是一个常见问题,因为 signal 有个 error 类型的常量,任何操作合并 signal (或者是 signal producers)就必须要求他们之间的 error 类型是相匹配的。使用 mapError 转化 searchText.producer 可能产生的任意错误为一个 NSError,与管道中其他 signal 相匹配。

再往下,signal 被过滤(filter)和节流(throttle)。如果 searchText 多次变动,这可以减少 signal 的产生。

然后 signal 被 flat-mapped 到另一个 signal,根据给定的 text 搜索 twitter。注意我已经打破了 flatMap 操作。这个是由于重载了原生的操作符,使得编译器无法决定该执行那个。马上我会在 Github 提一个 issue。

为 UIKit 添加可变属性

当前 RAC 3 缺少一些 UIKit 集成,我猜想它会在下个 beta 版出现。现在,为了给视图绑定一个 ViewModel,你不得不自己添加 extension。幸运的是这很简单。

在另外一个项目,我已经创建了一个有用的函数,用来懒加载关联属性:

这里创建了一个 T 类型的属性,给定的比例函数式用于第一次访问初始化属性。

这个可以被用于懒加载一个可变属性:

上面的代码创建了一个可变的属性,然后向 producer 订阅,当值改变的时候调用 setter 函数。

这个可以被用于添加 RAC3 属性到视图上,如下: