ReactiveCocoa 和 MVVM 入门

ReactiveCocoa 简单介绍

  • 观察值

你别动,你一动我就知道。

1
2
3
4
5
@weakify(self);
[RACObserve(self, value) subscribeNext:^(NSString* x) {
@strongify(self);
NSLog(@"你动了");
}];
  • 单边

    你唱歌,我就跳舞

1
2
3
4
5
6
7
8
9
10
11
RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"唱歌"];
[subscriber sendCompleted];
return nil;
}];
RAC(self, value) = [signalA map:^id(NSString* value) {
if ([value isEqualToString:@"唱歌"]) {
return @"跳舞";
}
return @"";
}];
  • 双边

    你向西,他就向东,他向左,你就向右。

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
RACChannelTerminal *channelA = RACChannelTo(self, valueA);
RACChannelTerminal *channelB = RACChannelTo(self, valueB);
[[channelA map:^id(NSString *value) {
if ([value isEqualToString:@"西"]) {
return @"东";
}
return value;
}] subscribe:channelB];
[[channelB map:^id(NSString *value) {
if ([value isEqualToString:@"左"]) {
return @"右";
}
return value;
}] subscribe:channelA];
[[RACObserve(self, valueA) filter:^BOOL(id value) {
return value ? YES : NO;
}] subscribeNext:^(NSString* x) {
NSLog(@"你向%@", x);
}];
[[RACObserve(self, valueB) filter:^BOOL(id value) {
return value ? YES : NO;
}] subscribeNext:^(NSString* x) {
NSLog(@"他向%@", x);
}];
self.valueA = @"西";
self.valueB = @"左";
  • 代理

    你是程序员,你帮我写个app吧

1
2
3
@protocol Programmer <NSObject>
- (void)makeAnApp;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RACSignal *ProgrammerSignal =
[self rac_signalForSelector:@selector(makeAnApp)
fromProtocol:@protocol(Programmer)];
[ProgrammerSignal subscribeNext:^(RACTuple* x) {
NSLog(@"花了一个月,app写好了");
}];
[self makeAnApp];
RACSignal *ProgrammerSignal =
[self rac_signalForSelector:@selector(makeAnApp)
fromProtocol:@protocol(Programmer)];
[ProgrammerSignal subscribeNext:^(RACTuple* x) {
NSLog(@"花了一个月,app写好了");
}];
[self makeAnApp];
  • 广播

    知道你的频道,我就能听到你了。

1
2
3
4
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:@"代码之道频道" object:nil] subscribeNext:^(NSNotification* x) {
NSLog(@"技巧:%@", x.userInfo[@"技巧"]);
}];
[[NSNotificationCenter defaultCenter] postNotificationName:@"代码之道频道" object:nil userInfo:@{@"技巧":@"用心写"}];
  • 节流

    不好意思,这里一秒钟只能通过一个人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"旅客A"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[subscriber sendNext:@"旅客B"];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[subscriber sendNext:@"旅客C"];
[subscriber sendNext:@"旅客D"];
[subscriber sendNext:@"旅客E"];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[subscriber sendNext:@"旅客F"];
});
return nil;
}] throttle:1] subscribeNext:^(id x) {
NSLog(@"%@通过了",x);
}];
// Test[2618:83764] 旅客A
//Test[2618:83764] 旅客B
// Test[2618:83764] 旅客E
// Test[2618:83764] 旅客F
  • 连接

    生活是一个故事接一个故事

1
2
3
4
5
6
7
8
9
10
11
12
13
RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"我恋爱啦"];
[subscriber sendCompleted];
return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"我结婚啦"];
[subscriber sendCompleted];
return nil;
}];
[[signalA concat:signalB] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
  • 合并

    污水都应该流入污水处理厂被处理

1
2
3
4
5
6
7
8
9
10
11
RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"纸厂污水"];
return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"电镀厂污水"];
return nil;
}];
[[RACSignal merge:@[signalA, signalB]] subscribeNext:^(id x) {
NSLog(@"处理%@",x);
}];
  • 映射

    我可以点石成金。

1
2
3
4
5
6
7
8
9
10
11
12
RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"石"];
return nil;
}] map:^id(NSString* value) {
if ([value isEqualToString:@"石"]) {
return @"金";
}
return value;
}];
[signal subscribeNext:^(id x) {
NSLog(@"%@", x);
}];
  • 过滤

    未满十八岁,禁止进入。

1
2
3
4
5
6
7
8
9
10
11
12
13
[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@(15)];
[subscriber sendNext:@(17)];
[subscriber sendNext:@(21)];
[subscriber sendNext:@(14)];
[subscriber sendNext:@(30)];
return nil;
}] filter:^BOOL(NSNumber* value) {
return value.integerValue >= 18;
}] subscribeNext:^(id x) {
NSLog(@"%@", x);
}];
}];
  • 扁平

    打蛋液,煎鸡蛋,上盘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"打蛋液");
[subscriber sendNext:@"蛋液"];
[subscriber sendCompleted];
return nil;
}] flattenMap:^RACStream *(NSString* value) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"把%@倒进锅里面煎",value);
[subscriber sendNext:@"煎蛋"];
[subscriber sendCompleted];
return nil;
}];
}] flattenMap:^RACStream *(NSString* value) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"把%@装到盘里", value);
[subscriber sendNext:@"上菜"];
[subscriber sendCompleted];
return nil;
}];
}] subscribeNext:^(id x) {
NSLog(@"%@", x);
}];
  • 重放

    一次制作,多次观看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RACSignal *replaySignal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"大导演拍了一部电影《我的男票是程序员》");
[subscriber sendNext:@"《我的男票是程序员》"];
return nil;
}] replay];
[replaySignal subscribeNext:^(id x) {
NSLog(@"小明看了%@", x);
}];
[replaySignal subscribeNext:^(id x) {
NSLog(@"小红也看了%@", x);
}];
//Test[1854:54712] 大导演拍了一部电影《我的男票是程序员》
//Test[1854:54712] 小明看了《我的男票是程序员》
//Test[1854:54712] 小红也看了《我的男票是程序员》
  • 重试

    成功之前可能需要数百次失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__block int failedCount = 0;
[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
if (failedCount < 100) {
failedCount++;
NSLog(@"我失败了");
[subscriber sendError:nil];
}else{
NSLog(@"经历了数百次失败后");
[subscriber sendNext:nil];
}
return nil;
}] retry] subscribeNext:^(id x) {
NSLog(@"终于成功了");
}];
//Test[2411:77080] 我失败了
// Test[2411:77080] 我失败了
//.....
// Test[2411:77080] 经历了数百次失败后
// Test[2411:77080] 终于成功了

以上


下面是 MVVM 与 ReactiveCocoa 项目实例演示
摘录翻译自ReactiveCocoa and MVVM, an Introduction


定义MVVM


  • Model - model 在 MVVM 中没有真正的变化. 取决于你的偏好, 你的 model 可能会或可能不会封装一些额外的业务逻辑工作. 我更倾向于把它当做一个容纳表现数据-模型对象信息的结构体, 并在一个单独的管理类中维护的创建/管理模型的统一逻辑.

  • View - view 包含实际 UI 本身(不论是 UIView 代码, storyboard 和 xib), 任何视图特定的逻辑, 和对用户输入的反馈. 在 iOS 中这不仅需要 UIView 代码和那些文件, 还包括很多需UIViewController 处理的工作.

  • View-Model - 这个术语本身会带来困惑, 因为它混搭了两个我们已知的术语, 但却是完全不同的东东. 它不是传统数据-模型结构中模型的意思(又来了, 只是我喜欢这个例子). 它的职责之一就是作为一个表现视图显示自身所需数据的静态模型;但它也有收集, 解释和转换那些数据的责任. 这留给了 view (controller) 一个更加清晰明确的任务: 呈现由 view-model 提供的数据.

View-Model 和 View Controller, 在一起,但独立


  • 有一个让用户输入他们姓名的 UITextField , 和一个写着 “Go” 的 UIButton
  • 有显示被查看的当前用户头像和姓名的 UIImageViewUILabel 各一个
  • 下面放着一个显示最新回复推文的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 获取的数据将用来:

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

视图控制器将对 view-model 起如下作用:

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

视图控制器不做的事儿:

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

进入 ReactiveCocoa



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

RACSignal

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

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

用 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
// 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
9
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 中的一个问题进行变通. tweetsallTweetsLoaded 这两个属性我们都需要追踪, 万一 tweets 变化和 allTweetsLoaded 为否(不管怎样我们都得重新加载表格). 有时两个属性都将在同一准确的时间发生变化, 意味着合并后的大信号中的两个信号都会发送一个值, 那么 reloadData 方法将会在同一个运行循环中被调用两次. UIKit 不喜欢这样. bufferWithTime: 在给明的时间内抓取所有下一个到来的值, 当给定的时间过后将所有值合在一起发给订阅者. 通过传入0作为时间, bufferWithTime: 将会抓取那个合并信号在特定的运行循环中发出的全部值, 并将他们一起发送出去. (NSTimer 以同样的方式工作, 这不是巧合, 因为 bufferWithTime: 是用 NSTimer 构建的. )暂时不用担心 scheduler, 试把它想做指明这些值必须在主线程上被发送. 现在我们确保 reloadData 每次运行循环只被调用一次.

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

1
2
3
4
5
[[self.goButton rac_signalForControlEvents: UIControlEventTouchUpInside]
subscribeNext: ^(id value) {
@strongify(self);
[self.viewModel 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 依然没有出过问题.