ReactiveCocoa 和 MVVM 入门

MVC

任何一个正经开发过一阵子软件的人都熟悉MVC. 它意思是Model View Controller, 是一个在复杂应用设计中组织代码的公认模式. 它也被证明在 iOS 开发中有着第二种含义:  Massive View Controller(重量级视图控制器). 它让许多程序员绞尽脑汁如何去使代码被解耦和组织地让人满意. 总的来讲, iOS 开发者已经得出结论: 他们须要给视图控制器瘦身, 并进一步分离事物;但该怎么作呢? html

MVVM

因而MVVM流行起来, 它表明Model View View-Model, 它在这帮助咱们建立更易处理, 更佳设计的代码. react

有时候违背苹果建议的编码方式并非个好作法. 我不是说不同意这样子, 我指的是可能会弊大于利. 好比我不建议你去实现个本身的 view controller 基类并试着本身处理视图生命周期. ios

带着这种情绪, 我想提个问题: 使用除苹果推荐的 MVC 以外的应用设计模式是愚蠢的么?git

. 有两个缘由. 程序员

  1. 苹果没有为解决重量级试图控制器问题提供真正的指导. 他们留给咱们来解决如何向代码添加更多清晰的思路. 用 MVVM 来实现这个目的想必是极好哒. (在今年 WWDC 的一些视频中, 苹果工程师在屏幕上的示例代码的确少量出现了 view-model, 不知道是否由于有它才成为了示例代码)
  2. MVVM, 至少是我将要在这里展现的 MVVM 的风格, 都跟 MVC 十分兼容. 仿佛咱们将 MVC 进行到下一个逻辑步骤. 

我不会说起 MVC/MVVM 的历史, 由于其余地方已经有所介绍, 而且我也不精通. 我将会关注如何用它进行 iOS/Mac 开发. github

定义 MVVM

  1. Model - model 在 MVVM 中没有真正的变化. 取决于你的偏好, 你的 model 可能会或可能不会封装一些额外的业务逻辑工做. 我更倾向于把它当作一个容纳表现数据-模型对象信息的结构体, 并在一个单独的管理类中维护的建立/管理模型的统一逻辑. 
  2. View - view 包含实际 UI 自己(不管是 UIView 代码, storyboard 和 xib), 任何视图特定的逻辑, 和对用户输入的反馈. 在 iOS 中这不只须要 UIView 代码和那些文件, 还包括不少需由 UIViewController 处理的工做. 
  3. View-Model - 这个术语自己会带来困惑, 由于它混搭了两个咱们已知的术语, 但倒是彻底不一样的东东. 它不是传统数据-模型结构中模型的意思(又来了, 只是我喜欢这个例子). 它的职责之一就是做为一个表现视图显示自身所需数据的静态模型;但它也有收集, 解释和转换那些数据的责任. 这留给了 view (controller) 一个更加清晰明确的任务: 呈现由 view-model 提供的数据. 

关于 view-model 的更多内容

view-model 一词的确不能充分表达咱们的意图. 一个更好的术语多是 “View Coordinator”(感谢Dave Lee提的这个 “View Coordinator” 术语, 真是个好点子). 你能够认为它就像是电视新闻主播背后的研究人员和做家团队. 它从必要的资源(数据库, 网络服务调用, 等)中获取原始数据, 运用逻辑, 并处理成 view (controller) 的展现数据. 它(一般经过属性)暴露给视图控制器须要知道的仅关于显示视图工做的信息(理想地你不会暴漏你的 data-model 对象). 它还负责对上游数据的修改(好比更新模型/数据库, API POST 调用). objective-c

MVC 世界中的 MVVM

我认为 MVVM 这个首字母缩写如同 view-model 术语同样, 对如何使用它们进行 iOS 开发体现得有点不太准确. 让咱们再检查下这个首字母缩写, 了解下它是怎么与 MVC 融为一体的. shell

为了图解表示, 咱们颠倒了 MVC 中的 V 和 C, 因而首字母缩写更能准确地反映出组件间的关系方位, 给咱们带来 MCV. 我也会对 MVVM 这么干, 将 V(iew) 移到 VM 的右边最终成为了 MVMV. (我相信这些首字母缩写起初不排成这样更合理的顺序是有缘由的. ) 数据库

这是这两种模式如何在 iOS 中组装在一块儿的简单映射: 编程

  • 我试图遵循区块尺寸(很是)大体对应它们负责的工做量. 
  • 注意到视图控制器有多大?
  • 你能够看到咱们巨大的视图控制器和 view-model 之间有大块工做上的重合. 
  • 你也能够看看视图控制器在 MVVM 中的足迹有多大一部分是跟视图重合的. 

你大可安心获知咱们并无真的去除视图控制器的概念或抛弃 “controller” 术语来匹配 MVVM. (唷. )咱们正要将重合的那块工做剥离到 view-model 中, 并让视图控制器的生活更加简单. 

咱们实际上最终以 MVMCV 了结. Model View-Model Controller View. 我确信我无拘无束的应用设计模式骇客行为会让人大吃一惊. 

咱们的结果: 

如今视图控制器仅关注于用 view-model 的数据配置和管理各类各样的视图, 并在先关用户输入时让 view-model 获知并须要向上游修改数据. 视图控制器不须要了解关于网络服务调用, Core Data, 模型对象等. (事实上有时经过 view-model 头文件而不是复制一大堆属性来暴漏 model 是很务实的, 后面还会有) 

view-model 会在视图控制器上以一个属性的方式存在. 视图控制器知道 view-model 和它的公有属性, 可是 view-model 对视图控制器一无所知. 你早就该对这个设计感受好多了由于咱们的关注点在这儿进行更好地分离. 

帮助你理解咱们如何把组件组装在一块儿还有组件对应职责的另外一种方式, 就是着眼于咱们新的应用构建模块层级图. 

(感谢Dave Lee @kastiglione)

View-Model 和 View Controller, 在一块儿,但独立

咱们来看个简单的 view-model 头文件来对咱们新构件的长相有个更好地概念. 为了情节简单, 咱们构建按了一个伪造的推特客户端来查看任何推特用户的最新回复, 经过输入他们的姓名并点击 “Go”. 咱们的样例界面将会是这样: 

  • 有一个让用户输入他们姓名的 UITextField , 和一个写着 “Go” 的 UIButton
  • 有显示被查看的当前用户头像和姓名的 UIImageView 和 UILabel 各一个
  • 下面放着一个显示最新回复推文的 UITableView
  • 容许无限滚动

View-Model 实例

咱们的 view-model 头文件应该长这样: 

1
2
3
4
5
6
7
8
9
10
11
12
13
//MYTwitterLookupViewModel.h
@interface MYTwitterLookupViewModel: NSObject

@property (nonatomic, assign, readonly, getter=isUsernameValid) BOOL usernameValid;
@property (nonatomic, strong, readonly) NSString *userFullName;
@property (nonatomic, strong, readonly) UIImage *userAvatarImage;
@property (nonatomic, strong, readonly) NSArray *tweets;
@property (nonatomic, assign, readonly) BOOL allTweetsLoaded;

@property (nonatomic, strong, readwrite) NSString *username;

- (void) getTweetsForCurrentUsername;
- (void) loadMoreTweets;

至关直截了当的填充. 注意到这些壮丽的 readonly 属性了么?这个 view-model 暴漏了视图控制器所必需的最小量信息, 视图控制器实际上并不在意 view-model 是如何得到这些信息的. 如今咱们二者都不在意. 仅仅假定你习惯于标准的网络服务请求, 校验, 数据操做和存储. 

view-model 不作的事儿

  • 对视图控制器以任何形式直接起做用或直接通告其变化

View Controller(视图控制器)

视图控制器从 view-model 获取的数据将用来:

  • 当 usernameValid 的值发生变化时触发 “Go” 按钮的 enabled 属性
  • 当 usernameValid 等于 NO 时调整按钮的 alpha 值为0. 5(等于 YES 时设为1. 0)
  • 更新 UILable 的 text 属性为字符串 userFullName 的值
  • 更新 UIImageView 的 image 属性为 userAvatarImage 的值
  • 用 tweets 数组中的对象设置表格视图中的 cell (后面会提到)
  • 当滑到表格视图底部时若是 allTweetsLoaded 为 NO, 提供一个 显示 “loading” 的 cell

视图控制器将对 view-model 起以下做用:

  • 每当 UITextField 中的文本发生变化, 更新 view-model 上仅有的 readwrite 属性 username
  • 当 “Go” 按钮被按下时调用 view-model 上的 getTweetsForCurrentUsername 方法
  • 当到达表格中的 “loading” cell 时调用 view-model 上的 loadMoreTweets 方法

视图控制器不作的事儿:

  • 发起网络服务调用
  • 管理 tweets 数组
  • 断定 username 内容是否有效
  • 将用户的姓和名格式化为全名
  • 下载用户头像并转成 UIImage(若是你习惯在 UIImageView 上使用类别从网络加载图片, 你能够暴漏 URL 而不是图片. 这样就给 view-model 与 UIKit 之间一个更清晰的划分, 但我视 UIImage 为数据而非数据的确切显示. 这些东西不是固定死的. )
  • 流汗

请再次注意视图控制器总的责任是处理 view-model 中的变化. 

子 View-Model

我提到过使用 view-model 上的 tweets 数组中的对象配置表格视图的 cell.一般你会期待展示 tweets 的是数据-模型对象. 你可能已经对其感到奇怪, 由于咱们试图经过 MVVM 模式不暴漏数据-模型对象. (前面提到过的) 

view-model 没必要在屏幕上显示全部东西. 你可用子 view-model 来表明屏幕上更小, 更潜在被封装的部分. 若是一个视图上的一小块儿(好比表格的 cell)在 app 中能够被重用以及(或)表现多个数据-模型对象, 子 view-model 会格外有利. 

你不老是须要子 view-model. 好比, 我可能用表格 header 视图来渲染咱们“tweetboat plus”应用的顶部. 它不是个可重用的组件, 因此我可能仅是将咱们已经给视图控制器用过的相同的 view-model 传给那个自定义的 header 视图. 它会用到 view-model 中它须要的信息, 而无视余下的部分. 这对于保持子视图同步是极好的方式, 由于它们能够有效地与信息中相同确切的上下文做用, 并观察确切相同属性的更新. 

在咱们的例子中,  tweets 数组将会被下面这样的子 view-model 充满: 

1
2
3
4
5
6
//MyTweetCellViewModel.h
@interface MYTweetCellViewModel: NSObject

@property (nonatomic, strong, readonly) NSString *tweetAuthorFullName;
@property (nonatomic, strong, readonly) UIImage *tweetAuthorAvatarImage;
@property (nonatomic, strong, readonly) NSString *tweetContent;

你可能认为这也太像普通”推特”里的数据-模型对象了吧. 为啥要干将其转化成 view-model 的工做?即便相似, view-model 让咱们限制信息只暴露给咱们须要的地方, 提供额外数据转换的属性, 或为特定的视图计算数据. (此外, 当能够不暴露可变数据-模型对象时也是极好的, 由于咱们但愿 view-model 本身承担起更新它们的任务, 而不是靠视图或视图控制器. ) 

View-Model 从哪来?

那么 view-model 是什么时候何处被建立的呢?视图控制器建立它们本身的 view-model 么? 

View-Model 产生 View-Model

严格来讲, 你应该为 app delegate 中的顶级视图控制器建立一个 view-model. 当展现一个新的视图控制器时, 或很小的视图被 view-model 表现时, 你应要求当前的 view-model 为你建立一个子 view-model. 

加入咱们想要在用户轻拍应用顶部的头像时添加一个资料视图控制器. 咱们能够为一级 view-model 添加相似以下方法: 

1
- (MYTwitterUserProfileViewModel *) viewModelForCurrentUser;

而后在咱们的一级视图控制器中这么用它: 

1
2
3
4
5
6
7
8
9
//MYMainViewController.m 
- (IBAction) didTapPrimaryUserAvatar
{
MYTwitterUserProfileViewModel *userProfileViewModel = [self.viewModel viewModelForCurrentUser];

MYTwitterUserProfileViewController *profileViewController =
[[MYTwitterUserProfileViewController alloc] initWithViewModel: userProfileViewModel];
[self.navigationController pushViewController: profileViewController animated:YES];
}

在这个例子中我将会展示当前用户的资料视图控制器, 可是个人资料视图控制器须要一个 view-model. 我这的主视图控制器不知道(也不应知道)用于建立关联相关用户 view-model 的所有必要数据, 因此它请求它本身的 view-model 来干这种建立新 view-model 的苦差事. 

View-Model 列表

至于咱们的推特 cell, 当数据驱动屏幕(在这个例子中或许是经过网络服务调用)聚到一块儿时, 我将会表明性地提早为对应的 cell 建立全部的 view-model. 因此在咱们这个方案中,  tweets 将会是一个 MYTweetCellViewModel 对象数组. 在个人表格视图中的 cellForRowAtIndexPath 方法中, 我将会在正确的索引上简单地抓取 view-model, 并把它赋值给个人 cell 上的 view-model 属性. 

Functional Core, Imperative Shell

view-model 这种通往应用设计的方法是一块应用设计之路上的垫脚石, 这种被称做“Functional Core, Imperative Shell”的应用设计由Gary Bernhardt创造. (我最近十分有幸去听Andy Matuschak关于这方面的演讲, 他为”胖的数值层, 瘦的对象层”提出充分理由. 虽然观点类似, 但关注于咱们怎样移除对象和它们状态的边界影响性质, 并用 Swift 中的新数据结构构建更加函数式, 可测试的数值层. )

Functional Core

view-model 就是 “functional core”, 尽管实际上在 iOS/Objective-C 中达到纯函数水平是很棘手的(Swift 提供了一些附加的函数性, 这会让咱们更接近). 大意是让咱们的 view-model 尽量少的对剩余的”应用世界”的依赖和影响. 那意味着什么?想起你第一次学编程时可能学到的简单函数吧. 它们可能接受一两个参数并输出一个结果. 数据输入, 数据输出.这个函数多是作一些数学运算或是将姓和名结合到一块儿. 不管应用的其余地方发生啥, 这个函数老是对相同的输入产生相同的输出. 这就是函数式方面. 

这就是咱们为 view-model 谋求的东西. 他们富有逻辑和转换数据并将结果存到属性的功能. 理想上相同的输入(好比网络服务响应)将会导出相同的输出(属性的值). 这意味着尽量多地消除由”应用世界”剩余部分带来的可能影响输出的因素, 好比使用一堆状态. 一个好的第一步就是不要再 view-model 头文件中引入 UIKit.h.(这是个重大原则, 但也有些灰色区域. 好比, 你可能认为 UIImage 是数据而不是展现信息. PS: 我爱这么干. 既然这样的话就得引入 UIKit. h 以便使用 UIImage 类)UIKit 其性质就是将要影响许多应用世界. 它包含不少”反作用”, 凭借改变一个值或调用一个函数将触发不少间接(甚至未知)的改变. 

更新: 刚刚看了 Andy 在函数式 Swift 会议上给出的另外一个超赞的演讲, 因而又想到了一些. 要清楚你的 view-model 仍然只是一个对象, 而不用维护一些状态(不然它将不会是你视图中很是好用的模型了. )但你仍该努力将尽量多的逻辑移到无状态的函数”值”中. 再重复一次, Swift在这方面比 Objective-C 更加可行. 

Imperative (Declarative?) Shell

命令式外壳 (Imperative Shell) 是咱们须要作全部的状态转换, 应用世界改变的苦差事的地方, 为的是将 view-model 数据转成给用户在屏幕上看到的东西. 这是咱们的视图(控制器), 实际上咱们在这分离 UIKit 的工做. 我仍将特别注意尽量消除状态并用 ReactiveCocoa 这种陈述性质的东西作这方面工做, 而 iOS 和 UIKit 在设计上是命令式的. (表格的 data source 就是个很好的例子, 由于它的委托模式强制将状态应用到委托中, 为了当请求发生时可以为表格视图提供信息. 实际上委托模式一般强制一大堆状态的使用)

可测试的核心

iOS 的单元测试是个脏, 苦, 乱的活儿. 至少我去作的时候得出的是这么个结论. 就这方面我还出读过一两本书, 但当开始作视图控制器的 mocking 和 swizzling 使其一些逻辑可测试时, 我目光呆滞. 我最终把单元测试纳入模型和任何同类别模型管理类中. (译者注: mock 是测试经常使用的手段, 而 method swizzling 是基于 Objective-C Runtime 交换方法实现的黑魔法) 

这个函数式核心同样的 view-model 的最大优势, 除了 bug 数量随着状态数递减以外, 就是变得很是可以进行单元测试. 若是你有那种每次输入相同而产生的输出也相同的方法, 那就很是适合单元测试的世界. 咱们如今将咱们的数据用获取/逻辑/转换提取出, 避免了视图控制器的复杂性. 那意味着构建棒棒哒测试时不须要用疯狂的 mock 对象, method swizzling, 或其余疯癫的变通方法(但愿能有). 

链接一切

那么当 view-model 的共有属性发生变化时咱们如何更新咱们的视图呢?

绝大部分时间咱们用对应的 view-model 来初始化视图控制器, 有点相似咱们刚刚在上文见到的: 

1
2
MYTwitterUserProfileViewController *profileViewController =
[[MYTwitterUserProfileViewController alloc] initWithViewModel: userProfileViewModel];

有时你没法在初始化时将 view-model 传入, 好比在 storyboard segue 或 cell dequeuing 的状况下. 这时你应该在讨论中的视图(控制器)中暴露一个公有可写的 view-model 属性. 

1
2
3
4
MYTwitterUserCell *cell =
[self.tableView dequeueReusableCellWithIdentifier: @"MYTwitterUserCell" forIndexPath: indexPath];
// grab the cell view-model from the vc view-model and assign it
cell.viewModel = self.viewModel. tweets[indexPath. row];

有时咱们能够在钩子程序调用前传入 view-model, 好比 init 和 viewDidLoad, 咱们能够从view-model 的属性初始化全部 UI 元素的状态. 

1
2
3
4
5
6
7
8
9
10
11
12
13
//dontDoThis1.m 
- (id) initWithViewModel:(MYTwitterLookupViewModel *) viewModel {
self = [super init];
if (!self) return nil;
_viewModel = viewModel;
return self;
}
- (void) viewDidLoad {
[super viewDidLoad];
_goButton.enabled = viewModel.isUsernameValid;
_goButton.alpha = viewModel.isUsernameValid ? 1 : 0.5;
// etc
}

好棒!咱们已经配置好了初始值. 当 view-model 上的数据改变时怎么办? 当”go” 按钮在何时可用了怎么办?当用户标签和头像在何时从网络上下载并填充了怎么办? 

咱们能够将视图控制器暴露给 view-model, 以便于当相关数据变化或相似事件发送时它能够调用一个 “updateUI” 方法. (别这么干. )在 view-model 上将视图控制器做为一个委托?当 view-model 内容有变化时发个通知?(不不不不. )

咱们的视图控制器会感知一些变化的发生. 咱们可使用从 UITextfield 得来的委托方法在每当有字符变化时经过检查 view-model 来更新按钮的状态. 

1
2
3
4
5
6
7
8
//dontDoThisEither.m
- (void)textFieldDidChange:(UITextField *)sender {
// update the view-model
self.viewModel.username = sender.text;
// check if things are now valid
self.goButton.enabled = self.viewModel.isUsernameValid;
self.goButton.alpha = self.viewModel.isUsernameValid ? 1.0 : 0.5;
}

这种方法解决的场景是在只有再文本框发生变化时才会影响 view-model 中的 isUsernameValid 值. 假使还有其余变量/动做改变 isUsernameValid 的状态将会怎么样?对于 view-model 中的网络调用会怎么样?或许咱们该为 view-model 上的方法加一个完成后回调处理, 这样咱们此时就能够更新 UI 的一切东西了?使用珍贵而笨重的 KVO 方法怎么样?

咱们或许最终使用多种多样咱们熟悉的机制将 view-model 和视图控制器全部的接触点都连起来, 但你已经知道了标题上不是这么写的. 这样在代码中建立了大量的入口点, 仅仅为了简单的更新 UI 就要在代码中彻底从新建立应用状态上下文. 

进入 ReactiveCocoa

ReactiveCocoa(RAC) 是来拯救咱们的, 并刚好返回给咱们一点理智. 让咱们看看如何作到. 

思考在一个新的用户页面上控制信息的流动, 当表单合法时更新提交按钮的状态. 你如今可能会照下面这么作: 

你最后经过使用状态, 当心翼翼地代码中许多不一样且零碎无关的内容穿到简单的逻辑上. 看看你信息流中全部不一样的入口点?(这还只是一个 UI 元素中的一条逻辑线. )咱们程序中如今用的抽象概念还不够厉害, 不能为咱们追踪全部事物的关系, 因此咱们中止本身去干这蛋疼事儿. 

让咱们看看陈述版本: 

这看起来可能像是为咱们应用流程文档中的一张老旧的计算机科学图解. 经过陈述式的编程, 咱们使用了更高层次的抽象, 来让咱们实际编程更靠近咱们在脑海中设计流程的方式. 咱们让电脑为咱们作更多工做. 实际的代码更加像这幅图了. 

RACSignal

RACSignal (信号)就 RAC 来讲是构造单元. 它表明咱们最终将要收到的信息. 当你能将将来某时刻收到的消息具体表示出来时, 你能够开始预先(陈述性)运用逻辑并构建你的信息流,而不是必须等到事件发生(命令式). 

信号会为了控制经过应用的信息流而得到全部这些异步方法(委托, 回调 block, 通知, KVO, target/action 事件观察, 等)并将它们统一到一个接口下.这只是直观理解. 不只是这些, 由于信息会流过你的应用, 它还提供给你轻松转换/分解/合并/过滤信息的能力. 

那么什么是信号呢?这是一个信号:

信号是一个发送一连串值的物体. 可是咱们这儿的信号啥也不干, 由于它尚未订阅者. 若是有订阅者监听时(已订阅)信号才会发信息. 它将会向那个订阅者发送0或多个载有数值的”next”事件, 后面跟着一个”complete”事件或一个”error”事件. (信号相似于其余语言/工具包中的 “promise”, 但更强大, 由于它不只限于向它的订阅者一次只传递一个返回值. ) 

正如我以前提到的, 若是以为须要的话你能够过滤, 转换, 分解和合并那些值. 不一样的订阅者可能须要使用信号经过不一样方式发送的值. 

信号发送的值是从哪得到的?

信号是一些等待某事发生的异步代码, 而后把结果值发送给它们的订阅者. 你能够用 RACSignal 的类方法 createSignal: 手动建立信号: 

1
2
3
4
5
6
7
8
9
//networkSignal.m
RACSignal *networkSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NetworkOperation *operation = [NetworkOperation getJSONOperationForURL:@"http://someurl"];
[operation setCompletionBlockWithSuccess:^(NetworkOperation *theOperation, id *result) {
[subscriber sendNext:result];
[subscriber sendCompleted];
} failure:^(NetworkOperation *theOperation, NSError *error) {
[subscriber sendError:error];
}];

我在这用一个具备成功和失败 block (伪造)的网络操做建立了一个信号. (若是我想让信号在被订阅时才让网络请求发生, 还能够用 RACSignal 的类方法 defer. )我在成功的 block 里使用提供的 subscriber 对象调用 sendNext: 和 sendCompleted:方法, 或在失败的 block 中调用 sendError:. 如今我能够订阅这个信号并将在响应返回时接收到 json 值或是 error. 

幸运的是, RAC 的创造者实际上使用它们本身的库来建立真的事物(捉摸一下), 因此对于咱们在平常须要什么, 他们有很强烈的想法. 他们为咱们提供了不少机制, 来从咱们一般使用的现存的异步模式中拉取信号. 别忘了若是你有一个没有被某个内建信号覆盖到的异步任务, 你能够很容易地用 createSignal: 或相似方法来建立信号. 

一个被提供的机制就是 RACObserve() 宏. (若是你不喜欢宏, 你能够简单地看看罩子下面并用稍微多些冗杂的描述. 这也很是好. 在咱们获得 Swift 版本的替代以前, 这也有在 Swift 中使用 RAC 的解决方案. )这个宏是 RAC 中对 KVO 中那些悲惨的 API 的替代. 你只须要传入对象和你想观察的那个对象某属性的 keypath. 给出这些参数后,  RACObserve 会建立一个信号, 一旦它有了订阅者, 它就马上发送那个属性的当前值, 并在发送那个属性在这以后的任何变化. 

1
RACSignal *usernameValidSignal = RACObserve(self.viewModel, usernameIsValid);

这仅是提供用于建立信号的一个工具. 这里有几个当即可用的方式, 来从内置控制流机制中拉取信号: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//signals.m
RACSignal *controlUpdate = [myButton rac_signalForControlEvents:UIControlEventTouchUpInside];
// signals for UIControl events send the control event value (UITextField, UIButton, UISlider, etc)
// subscribeNext:^(UIButton *button) { NSLog(@"%@", button); // UIButton instance }

RACSignal *textChange = [myTextField rac_textSignal];
// some special methods are provided for commonly needed control event values off certain controls
// subscribeNext:^(UITextField *textfield) { NSLog(@"%@", textfield.text); // "Hello!" }

RACSignal *alertButtonClicked = [myAlertView rac_buttonClickedSignal];
// signals for some delegate methods send the delegate params as the value
// e.g. UIAlertView, UIActionSheet, UIImagePickerControl, etc
// (limited to methods that return void)
// subscribeNext:^(NSNumber *buttonIndex) { NSLog(@"%@", buttonIndex); // "1" }

RACSignal *viewAppeared = [self rac_signalForSelector:@selector(viewDidAppear:)];
// signals for arbitrary selectors that return void, send the method params as the value
// works for built in or your own methods
// subscribeNext:^(NSNumber *animated) { NSLog(@"viewDidAppear %@", animated); // "viewDidAppear 1" }

记住你也能轻松建立本身的信号, 包括替代那些没有内建支持的其余委托. 咱们如今可以从全部这些不连贯的异步/控制流工具中拉取出信号并将他们合并, 试想一想这该多酷!这些会成为咱们以前看到的陈述性图表中的节点. 真是兴奋. 

什么是订阅者?

简言之, 订阅者就是一段代码, 它等待信号给它发送一些值, 而后订阅者就能处理这些值了. (它也能够做用于 “complete” 和 “error” 事件. )

这有一个简单的订阅者, 是经过向信号的实例方法 subscribeNext 传入一个 block 来建立的. 咱们在这经过 RACObserve()宏建立信号来观察一个对象上属性的当前值, 并把它赋值给一个内部属性. 

1
2
3
4
5
6
7
8
9
- (void) viewDidLoad {
// . . .
// create and get a reference to the signal
RACSignal *usernameValidSignal = RACObserve(self.viewModel, isUsernameValid);
// update the local property when this value changes
[usernameValidSignal subscribeNext: ^(NSNumber *isValidNumber) {
self.usernameIsValid = isValidNumber. boolValue
}];
}

注意 RAC 只处理对象, 而不处理像 BOOL 这样的原始值. 不过不用担忧, RAC 一般会帮你这些转换. 

幸运的是 RAC 的创造者也意识到这种绑定行为的广泛必要性, 因此他们提供了另外一个宏 RAC(). 与 RACObserve() 相同, 你提供想要与即将到来的值绑定的对象和参数, 在其内部它所作的是建立一个订阅者并更新其属性的值. 咱们的例子如今看起来像这样: 

1
2
3
4
- (void) viewDidLoad {
//. . .
RAC(self, usernameIsValid) = RACObserve(self.viewModel, isUsernameValid);
}

考虑下咱们的目标, 这么干有点傻啊. 咱们不须要将信号发送的值存到属性中(这会建立状态), 咱们真正要作的是用从那个值获取到信息来更新 UI. 

转换数据流

如今咱们进入 RAC 为咱们提供的用于转换数值流的方法. 咱们将会利用 RACSignal 的实例方法 map

1
2
3
4
5
6
7
8
9
10
//transformingStreams.m
- (void) viewDidLoad {
//...
RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, isUsernameValid);
RAC(self.goButton, enabled) = usernameIsValidSignal;
RAC(self.goButton, alpha) = [usernameIsValidSignal
map:^id(NSNumber *usernameIsValid) {
return usernameIsValid.boolValue ? @1.0 : @0.5;
}];
}

这样如今咱们将 view-model 上的 isUsernameValid 发生的变化直接绑定到 goButton 的 enabled 属性上. 酷吧?对 alpha 的绑定更酷, 由于咱们正在使用 map 方法将值转换成与 alpha 属性相关的值. (注意在这里咱们返回的是一个 NSNumber 对象而不是原始float值. 这基本上是惟一的污点: 你须要负责为 RAC 将原始值转化为对象, 由于它不能帮你导出来. 

多个订阅者, 反作用, 昂贵的操做

订阅信号链时要明白重要的一件事是每当一个新值经过信号链被发送出去时, 实际上会给每一个订阅者都发送一次. 直到意识到这就咱们而言是有意义的, 信号发出的值不存储在任何地方(除了 RAC 在内部实现中). 当信号须要发送一个新的值时, 它会遍历全部的订阅者并给每一个订阅者发送那个值. (这是对信号链实际工做的简化说明, 但基本想法是对的) 

这为何重要?这意味着信号链某处存在的任何反作用, 任何影响应用世界的转变, 将会发生屡次. 这对新接触 RAC 的用户来讲是意想不到的. (这也违反了函数式构建的理念-数据输入, 数据输出). 

一个作做的例子多是: 信号链某处的信号在每次按钮被按下时更新 self 中的一个计数器属性. 若是信号链有多个订阅者, 计数器的增加将会比你想的还要多. 你须要努力从信号链中尽量剔除反作用. 当反作用不可避免时, 你可使用一些恰当的预防机制. 我将会在另外一篇文章中探索. 

除反作用以外, 你须要注意带有昂贵操做和可变数据的信号链. 网络请求就是一个三者兼得的例子: 

  1. 网络请求影响了应用的网络层(反作用). 
  2. 网络请求为信号链引入了可变数据. (两个彻底同样请求可能返回了不一样的数据. )
  3. 网络请求反应慢啊. 

例如, 你可能有个信号在每次按钮按下时发送一个值, 而你想将这个值转换成网络请求的结果. 若是有多个订阅者要这个处理信号链上返回的这个值, 你将发起多个网络请求. 

网络请求明显是常常须要的. 正如你所指望, RAC 提供这些状况的解决方案, 也就是 RACCommand 和多点广播. 我将会在下一篇文章中更深刻地分析. 

Tweetboat Plus

既然简短的介绍(嗯哼)扯远了, 让咱们着眼于如何用 ReactiveCocoa 将 view-model 与视图控制器链接起来. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//
// View Controller
//

- (void) viewDidLoad {
[super viewDidLoad];

RAC(self.viewModel, username) = [myTextfield rac_textSignal];

RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, usernameValid);

RAC(self.goButton, alpha) = [usernameIsValidSignal
map: ^(NSNumber *valid) {
return valid. boolValue ? @1 : @0. 5;
}];

RAC(self.goButton, enabled) = usernameIsValidSignal;

RAC(self.avatarImageView, image) = RACObserve(self.viewModel, userAvatarImage);

RAC(self.userNameLabel, text) = RACObserve(self.viewModel, userFullName);

@weakify(self);
[[[RACSignal merge: @[RACObserve(self.viewModel, tweets),
RACObserve(self.viewModel, allTweetsLoaded)]]
bufferWithTime: 0 onScheduler: [RACScheduler mainThreadScheduler]]
subscribeNext: ^(id value) {
@strongify(self);
[self.tableView reloadData];
}];

[[self.goButton rac_signalForControlEvents: UIControlEventTouchUpInside]
subscribeNext: ^(id value) {
@strongify(self);
[self.viewModel getTweetsForCurrentUsername];
}];
}

-(UITableViewCell*)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
// if table section is the tweets section
if (indexPath. section == 0) {
MYTwitterUserCell *cell =
[self.tableView dequeueReusableCellWithIdentifier: @"MYTwitterUserCell" forIndexPath: indexPath];

// grab the cell view model from the vc view model and assign it
cell.viewModel = self.viewModel. tweets[indexPath. row];
return cell;
} else {
// else if the section is our loading cell
MYLoadingCell *cell =
[self.tableView dequeueReusableCellWithIdentifier: @"MYLoadingCell" forIndexPath: indexPath];
[self.viewModel loadMoreTweets];
return cell;
}
}


//
// MYTwitterUserCell
//

// this could also be in cell init
- (void) awakeFromNib {
[super awakeFromNib];

RAC(self.avatarImageView, image) = RACObserve(self, viewModel. tweetAuthorAvatarImage);
RAC(self.userNameLabel, text) = RACObserve(self, viewModel. tweetAuthorFullName);
RAC(self.tweetTextLabel, text) = RACObserve(self, viewModel. tweetContent);
}

让咱们过一遍这个例子. 

1
RAC(self.viewModel, username) = [myTextfield rac_textSignal];

在这咱们用 RAC 库中的方法从 UITextField 拉取一个信号. 这行代码将 view-model 上的可读写属性 username 绑定到文本框上的用户输入的任何更新. 

1
2
3
4
5
6
7
8
RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, usernameValid);

RAC(self.goButton, alpha) = [usernameIsValidSignal
map: ^(NSNumber *valid) {
return valid. boolValue ? @1 : @0. 5;
}];

RAC(self.goButton, enabled) = usernameIsValidSignal;

在这咱们用 RACObserve 方法在 view-model 的 usernameValid 属性上建立了一个信号 usernameIsValidSignal. 不管什么时候属性发生变化, 它将会沿着管道发送一个新的 @YES 或 @NO. 咱们拿到那个值并将其绑定到 goButton 的两个属性上. 首先咱们将 alpha 分别对应 YES 或 NO 更新到1或0. 5(记着在这必须返回 NSNumber). 而后咱们直接将信号绑定到 enabled 属性, 由于 YES 和 NO 在这无需转换就能完美地运做. 

1
2
3
RAC(self.avatarImageView, image) = RACObserve(self.viewModel, userAvatarImage);

RAC(self.userNameLabel, text) = RACObserve(self.viewModel, userFullName);

下面咱们为表头的图像视图和用户标签建立绑定, 再次在 view-model 上对应的属性上用 RACObserve 宏建立信号. 

1
2
3
4
5
6
7
8
@weakify(self);
[[[RACSignal merge: @[RACObserve(self.viewModel, tweets),
RACObserve(self.viewModel, allTweetsLoaded)]]
bufferWithTime: 0 onScheduler: [RACScheduler mainThreadScheduler]]
subscribeNext: ^(id value) {
@strongify(self);
[self.tableView reloadData];
}];

这货看上去有点诡异, 因此咱们在这上多花点时间. 咱们想在 view-model 上 tweets 数组或 allTweetsLoaded 属性发生变化时更新表格视图. (在这个例子中, 咱们要用一个简单的方法来从新加载整张表. )因此咱们将这两个属性被观察后建立的两个信号合并成一个更大的信号, 当两个属性中有一个发生变化, 这个信号就会发送值. (你一向认为信号的值是同类型的, 不会像这个信号有同样混杂的值. 这极可能在 Swift 版本的 RAC 中强制要求, 但在这咱们不关心发出的真实值, 咱们只是用它来触发表格式图的从新加载. ) 

那么这儿看起来最吓人的部分多是信号链中的 bufferWithTime: onScheduler: 方法. 须要它来围绕 UIKit 中的一个问题进行变通.  tweets 和 allTweetsLoaded 这两个属性咱们都须要追踪, 万一 tweets 变化和 allTweetsLoaded 为否(无论怎样咱们都得从新加载表格). 有时两个属性都将在同一准确的时间发生变化, 意味着合并后的大信号中的两个信号都会发送一个值, 那么 reloadData 方法将会在同一个运行循环中被调用两次. UIKit 不喜欢这样.  bufferWithTime: 在给明的时间内抓取全部下一个到来的值, 当给定的时间事后将全部值合在一块儿发给订阅者. 经过传入0做为时间,  bufferWithTime: 将会抓取那个合并信号在特定的运行循环中发出的所有值, 并将他们一块儿发送出去. (NSTimer 以一样的方式工做, 这不是巧合, 由于 bufferWithTime: 是用 NSTimer 构建的. )暂时不用担忧 scheduler, 试把它想作指明这些值必须在主线程上被发送. 如今咱们确保 reloadData 每次运行循环只被调用一次. 

注意我在这用 @weakify/@strongify 宏切换 strong 和 weak. 这在建立全部这些 block 时很是重要. 在 RAC 的 block 中使用 self 时self 将会被捕获为强引用并获得保留环, 除非你尤为意识到要破除保留环

1
2
3
4
5
[[self.goButton rac_signalForControlEvents: UIControlEventTouchUpInside]
subscribeNext: ^(id value) {
@strongify(self);
[self.viewModel getTweetsForCurrentUsername];
}];

我将会在下一篇文章中在这里引入 RACCommand, 但目前咱们只是当按钮被触碰时手动调用 view-model 的 getTweetsForCurrentUsername 方法. 

咱们已经搞定了 cellForRowAtIndexPath 的第一部分, 那么我在这将只说下 loading cell: 

1
2
3
4
MYLoadingCell *cell =
[self.tableView dequeueReusableCellWithIdentifier: @"MYLoadingCell" forIndexPath: indexPath];
[self.viewModel loadMoreTweets];
return cell;

这是另外一块咱们之后将利用到 RACCommand 的地方, 但目前咱们只是调用 view-model 的 loadMoreTweets 方法. 咱们将只是信任若是 cell 显示或隐藏屡次的话 view-model 会避免屡次内部调用. 

1
2
3
4
5
6
7
- (void) awakeFromNib {
[super awakeFromNib];

RAC(self.avatarImageView, image) = RACObserve(self, viewModel. tweetAuthorAvatarImage);
RAC(self.userNameLabel, text) = RACObserve(self, viewModel. tweetAuthorFullName);
RAC(self.tweetTextLabel, text) = RACObserve(self, viewModel. tweetContent);
}

这段如今应该很是直接了, 除此以外我想指出一点. 咱们正在将图片和文字绑定到 UI 上对应的属性, 但注意 viewModel 出如今 RACObserve 宏中逗号右边. 这些 cell 终将被重用, 新的 view-models 将会被赋值. 若是咱们不将 viewModel 放在逗号右边, 那就会监听 viewModel 属性的变化而后每次都要从新设置绑定;若是放在逗号右边,  RACObserve 将会为咱们负责这些事儿. 所以咱们只须要设定一次绑定并让 Reactive Cocoa 作剩余的部分. 这是在绑定表格 cell 时为了性能须要记住的好东西. 我在实践中即便是有不少表格 cell 依然没有出过问题. 

福利-消除更多的状态

有时候你能够在 view-model 中暴露 RACSignal 对象来替代像字符串和图像这样的属性, 这能在 view-model 上消除更多的状态. 而后视图控制器就不须要本身用 RACObserve 建立信号了, 并只是直接影响这些信号. 要意识到若是你的信号在被 UI 订阅/绑定到 UI 以前发出过一个值, 那么你将不会收到那个”初始”的值. 

结论

本文篇幅略长, 但别被吓着. 这还有好多没讲的, 并且是干货儿, 是舒展你大脑的好方法. 这毫无疑问是不一样的编程风格. 花一下子功夫中止机械地试图用命令式方案去解决问题. 即便你一开始不是常常用这种编程风格, 我认为这有助于理解和提醒咱们有大相径庭的途径来解决咱们程序员的困惑. 

下一次我将稍微深刻 view-model 内部中本文没提到的内容, 并介绍下 RACCommand(但愿篇幅能短不少). 而后咱们将投入到一个真实案例中, 那是个人一个叫作Three Cents的 app 中的一个至关复杂的页面, 它混合了网络调用, CoreData, 多重 UI 状态, 等等!

拓展阅读

相关文章
相关标签/搜索