前言
学习 RACCommand 花了我很长一段时间,甚至一直搞不懂它的我一直想放弃学习 RAC,但是越是搞不懂,我就越想去征服它,于是就断断续续的看源码,上 Google 搜文章,在这个周末的最后时间终于看到一丝出路,基本弄懂了 RACCommand 到底是干什么的以及它是如何做到这件事的。
按照我的经验,要弄懂 RACCommand,你必须先熟悉 RAC 中下面的内容:
- RACSignal
- RACSubject
- RACMulticastConnection
- RACSignal + Operation
- RACScheduler
- 以及对 RAC 基本的
值流
概念的理解。
没错,你看 RACCommand 的源码不过就二百多行,但是要去完全的理解它可需要下不少功夫,但是没关系,很多时候如果你能先知道一个东西发明出来是为了解决什么问题,然后在一步步的去了解它解决这个问题的思路的话,那么理解它就会变得容易一些了。
理解 RACCommand
好,现在我就来说说 RACCommand 是用来干嘛的,我自己的理解就是:
RACCommand 是对一个动作的触发以及它产生的后续事件的封装。
看起来是比较抽象的,我来举个栗子,就是登陆的流程,这个例子在很多介绍 RAC 的文章里都会讲,但是他们通常是向你展示 RAC 有多方便而并没有讲 RACCommand,现在我通过这个例子来讲 RACCommand。
看看最简单的情况,有两个 textfield 用于输入用户名和密码,有一个 button 用于登陆。
我们把 button 的点击称为一个动作,但是通常来说有一个需求是,当用户没有输入用户名或密码时,button 需要被禁止点击。
也就是说这个动作是否能发生取决于一个条件,RACCommand 就封装了这样一个概念,相对应于它的一个属性:
@property (nonatomic, strong, readonly) RACSignal *enabled;
它是一个信号,传递的应该是布尔值流,当值为 true 是,动作就可以执行,反之。
接下来,当条件满足之后,点击 button 开始这个动作,产生一个事件,这时候第二个需求来了,如果多次点击 button,只有当第一个事件完成之后,另一个事件才被允许产生。
也就是说多次触发这个动作是否能让相同的事件同时执行,同样,RACCommand 也封装了这个概念:
@property (atomic, assign) BOOL allowsConcurrentExecution;
这个属性是一个布尔值,用于控制动作是否允许同时执行相同的事件。
然后事件就开始执行了,第三个需求是,我想知道这个事件是否在执行中,比如为了用于显示一个 HUD。
也就是说需要监控动作产生的事件执行的状态,当然,RACCommand 也为我们提供了这个属性:
@property (nonatomic, strong, readonly) RACSignal *executing;
这个属性也是一个 signal,这个 signal 也是会产生一系列的布尔值,当事件在执行过程中就会传递 true,结束了就传递 false。
很显然,我们是需要知道事件执行的结果的,RACCommand 为我们提供了另外一个属性供我们使用:
@property (nonatomic, strong, readonly) RACSignal *executionSignals;
看这个变量名,你可能会有一丝疑惑,为什么是复数,难道不是一个信号而是多个吗?没错,你是想法是对的,executionSignals
其实是一个信号的信号,什么意思呢?就是这个信号传递的不是普通的值,而是一个个信号。想想为什么?因为之前说过一个动作可能会被多次触发,而同时产生多个事件开始执行,这个信号传递的值就是用于传递事件执行的结果值的信号,所以也可能有多个。
但是某些时候事件可能也会执行失败,我们需要捕获那些错误的信息,怎么办?RACCommand 也专门提供了这样一个用于传递错误的信号:
@property (nonatomic, strong, readonly) RACSignal *errors;
这个信号就是用于传递事件失败的信号的。
但这里,RACCommand 的基本概念就讲完了。怎么样?是不是感觉对 RACCommand 的理解清晰了不少?
开始使用 RACCommand
前面已经提到了,要理解 RACCommand 的源码需要了解的 RAC 知识比较多,因为我认为 RACCommand 本身就是对 RAC 的一次实践,所以源码理解起来还是还是比较困难的,虽然代码不多。看完了基础的概念,我们还是看看如何使用 RACCommand 吧。
一般来说,我们会把一个 command 和 UIControl 绑定,RAC 已经给 UIControl 及其子类提供了这样一个属性 rac_command
,我们只需要初始化一个 command 并且赋值给它就可以完成绑定了,就像下面这样:
self.loginButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal empty]; // 这里返回一个需要的信号
}];
当然你还可以用指定一个 enable 信号的方式创建一个 command,这样的话,control 的 enable 属性也会和这个信号绑定。
这样绑定完成后,每次点击 button,command 就会开始执行。
除了这种方式外,我们还常常会把网络请求这种耗时的操作用 RACCommand 来实现,就像之前理解的,通过它我们可以很容易的对网络请求的状态进行监控,无论是参数
、请求状态
,还是 防止多次相同请求
、错误处理
这样的需求用 command 处理起来都比较方便。我们来看看实例:
首先,在 viewModel 中定义一个 command,
@property (nonatomic, strong) RACCommand *requestRealStuffCommand;
然后初始化它:
self.requestRealStuffCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(NSString *dayString) {
return [[[[GKHttpClient sharedClient] getGankRealStuffForOneDay:dayString] map:^id(NSDictionary *json) {
NSArray *categories = json[@"category"];
NSDictionary *result = json[@"results"];
return [[categories.rac_sequence map:^id(NSString *cate) {
return result[cate];
}] foldLeftWithStart:@[] reduce:^id(NSArray *accumulator, id value) {
return [accumulator arrayByAddingObjectsFromArray:value];
}];
}] mtl_mapToArrayOfModelsWithClass:[RealStuff class]];
}];
我这里用封装的 http client 来发起请求,并且直接将结果 map 为 model。
最后执行 command 发起请求,注意我这里还用到了 command 的参数这个特性。
[self.requestRealStuffCommand execute:self.history[day]];
接着你可以监听请求的执行状态:
[self.viewModel.requestRealStuffCommand.executing subscribeNext:^(NSNumber *x) {
[UIApplication sharedApplication].networkActivityIndicatorVisible = x.boolValue;
}];
请求完成后刷新列表:
[[self.viewModel.requestRealStuffCommand.executionSignals switchToLatest] subscribeNext:^(id x) {
@strongify(self)
[self.tableView reloadData];
}];
更具体的使用细节可以参看我的这个 demo。
网友评论