RACCommand
Is a relatively complex in ReactiveCocoa class, use ReactiveCocoa majority of people, especially beginners and do not use it often.
In many cases, although the use of RACSignal
and RACSubject
be able to solve most of the problems, but RACCommand
use can bring great convenience for us, especially in operations related to the side effects.
The article will not discuss the
RACCommand
parallel execution problems, which is ignoredallowsConcurrentExecution
andallowsConcurrentExecutionSubject
the presence, but they are indeedRACCommand
very important in here just to reduce unnecessary interference factors.
RACCommand Profile
Described in the previous articles in RACSignal
different other elements, RACCommand
it does not mean that the data stream, it's just an inheritance from NSObject
the class, but it can be used to create and subscribe to the response signal for certain events.
@interface RACCommand<__contravariant InputType, __covariant ValueType> : NSObject @end
It is not in itself a RACStream
or RACSignal
subclass, but a management for RACSignal
the creation and subscription class.
In ReactiveCocoa of the portion FrameworkOverview RACCommand
this explanation are:
A command, represented by the RACCommand class, creates and subscribes to a signal in response to some action. This makes it easy to perform side-effecting work as the user interacts with the app.
When used to interact or perform operations include side effects and UIKit components RACCommand
can help us faster processing and response tasks, reduce the complexity of coding and engineering.
RACCommand initialization and execution
In -initWithSignalBlock:
the method signature method, you can see that each time RACCommand
will pass in a type of initialization RACSignal<ValueType> * (^)(InputType _Nullable input)
of signalBlock
:
- (instancetype)initWithSignalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;
Input for the InputType
return value RACSignal<ValueType> *
, and InputType
that is to call -execute:
the incoming object when the method:
- (RACSignal<ValueType> *)execute:(nullable InputType)input;
This is RACCommand
an external variable (or "side-effects") passed to the method of internal ReactiveCocoa, you can be understood as RACCommand
an external variable InputType
converted to use RACSignal
wrapped ValueType
object.
Take the following code as an example, first look at RACCommand
how this works:
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(NSNumber * _Nullable input) { return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { NSInteger integer = [input integerValue]; for (NSInteger i = 0; i < integer; i++) { [subscriber sendNext:@(i)]; } [subscriber sendCompleted]; return nil; }]; }]; [[command.executionSignals switchToLatest] subscribeNext:^(id _Nullable x) { NSLog(@"%@", x); }]; [command execute:@1]; [RACScheduler.mainThreadScheduler afterDelay:0.1 schedule:^{ [command execute:@2]; }]; [RACScheduler.mainThreadScheduler afterDelay:0.2 schedule:^{ [command execute:@3]; }];
First, using the -initWithSignalBlock:
method to create an RACCommand
object, passing a type InputType -> RACSignal<ValueType>
of Block, in accordance with the input signal corresponding to the number of messages sent, if running the above code, prints out:
0 0 1 0 1 2
-switchToLatest
The method of operation can only signal a .
Every time executionSignals
when sending a new signal, the switchToLatest
method returns the signal will subscribe to the new signal, here also to ensure that the value of the latest signal every time the print out.
In the code above have one last question to be answered, why use RACScheduler.mainThreadScheduler
after a delayed call -execute:
method? Because by default RACCommand
do not support concurrent operations, it need only be one operation after the last command is sent to the next, otherwise it will return an error signal RACErrorSignal
, these errors can subscribe to command.errors
get.
If you do use the following several ways -execute:
methods:
[command execute:@1]; [command execute:@2]; [command execute:@3];
I believe, no accident, then, you can only see the output in the console 0
.
The most important internal 'signal'
RACCommand
The most important internal "signal" is addedExecutionSignalsSubject
:
@property (nonatomic, strong, readonly) RACSubject *addedExecutionSignalsSubject;
This RACSubject
object is derived by a variety of operating almost all RACCommand
the other signals, we will specifically described in the next section;
Since it addedExecutionSignalsSubject
is a RACSubject
, it is not the default when you create a good news for subscribers to send, it will accept the data and where it pushed to subscribers? The answer lies in -execute:
the method:
- (RACSignal *)execute:(id)input { BOOL enabled = [[self.immediateEnabled first] boolValue]; if (!enabled) { NSError *error = [NSError errorWithDomain:RACCommandErrorDomain code:RACCommandErrorNotEnabled userInfo:@{ NSLocalizedDescriptionKey: NSLocalizedString(@"The command is disabled and cannot be executed", nil), RACUnderlyingCommandErrorKey: self }]; return [RACSignal error:error]; } RACSignal *signal = self.signalBlock(input); RACMulticastConnection *connection = [[signal subscribeOn:RACScheduler.mainThreadScheduler] multicast:[RACReplaySubject subject]]; [self.addedExecutionSignalsSubject sendNext:connection.signal]; [connection connect]; return [connection.signal setNameWithFormat:@"%@ -execute: %@", self, RACDescription(input)]; }
In the method where you can see the successive implementation of -execute:
methodological reasons we can not be successful: each time the implementation of this approach will be another signal from immediateEnabled
the reading whether the execution of the current command's BOOL
value, if not implemented, directly return RACErrorSignal
.
-execute:
The method is the only one for theaddedExecutionSignalsSubject
method of production information.
In execution signalBlock
returns RACSignal
after, it will be packaged into a current signal RACMulticastConnection
, and then call the -sendNext:
method to send to addedExecutionSignalsSubject
the Executive -connect
way to subscribe to the original signal, the final return.
Complex initialization
与简单的 -execute:
方法相比,RACCommand
的初始化方法就复杂多了,虽然我们在方法中传入了 signalBlock
,但是 -initWithEnabled:signalBlock:
方法只是对这个 block 进行了简单的 copy
,真正使用这个 block 的还是上一节中的 -execute:
方法中。
由于 RACCommand
在初始化方法中初始化了七个高阶信号,它的实现非常复杂:
- (instancetype)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal<id> * (^)(id input))signalBlock { self = [super init]; _addedExecutionSignalsSubject = [RACSubject new]; _signalBlock = [signalBlock copy]; _executionSignals = ...; _errors = ...; RACSignal *immediateExecuting = ...; _executing = ...; RACSignal *moreExecutionsAllowed = ...; _immediateEnabled =...; _enabled = ...; return self; }
这一小节并不能完全介绍全部的七个信号的实现,只会介绍其中的 immediateExecuting
和 moreExecutionsAllowed
两个临时信号,剩下的信号都会在下一节中分析。
表示当前有操作执行的信号
首先是 immediateExecuting
信号:
RACSignal *immediateExecuting = [[[[self.addedExecutionSignalsSubject flattenMap:^(RACSignal *signal) { return [[[signal catchTo:[RACSignal empty]] then:^{ return [RACSignal return:@-1]; }] startWith:@1]; }] scanWithStart:@0 reduce:^(NSNumber *running, NSNumber *next) { return @(running.integerValue + next.integerValue); }] map:^(NSNumber *count) { return @(count.integerValue > 0); }] startWith:@NO];
immediateExecuting
是一个用于表示当前是否有任务执行的信号,如果输入的 addedExecutionSignalsSubject
等价于以下的信号:
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { [subscriber sendNext:[RACSignal error:[NSError errorWithDomain:@"Error" code:1 userInfo:nil]]]; [subscriber sendNext:[RACSignal return:@1]]; [subscriber sendNext:[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { [RACScheduler.mainThreadScheduler afterDelay:1 schedule:^ { [subscriber sendCompleted]; }]; return nil; }]]; [subscriber sendNext:[RACSignal return:@3]]; [subscriber sendCompleted]; return nil; }];
在本文的所有章节中都会假设输入的
addedExecutionSignalsSubject
信号跟上面的代码返回的完全相同。
那么,最后生成的高阶信号 immediateExecuting
如下:
-catchTo:
将所有的错误转换成RACEmptySignal
信号;-flattenMap:
将每一个信号的开始和结束的时间点转换成1
和-1
两个信号;-scanWithStart:reduce:
从0
开始累加原有的信号;-map:
将大于1
的信号转换为@YES
;-startWith:
在信号序列最前面加入@NO
,表示在最开始时,没有任何动作在执行。
immediateExecuting
使用几个 RACSignal
的操作成功将原有的信号流转换成了表示是否有操作执行的信号流。
表示是否允许更多操作执行的信号
相比于 immediateExecuting
信号的复杂,moreExecutionsAllowed
就简单多了:
RACSignal *moreExecutionsAllowed = [RACSignal if:[self.allowsConcurrentExecutionSubject startWith:@NO] then:[RACSignal return:@YES] else:[immediateExecuting not]];
因为文章中不准备介绍与并发执行有关的内容,所以这里的 then
语句永远不会执行,既然 RACCommand
不支持并行操作,那么这段代码就非常好理解了,当前 RACCommand
能否执行操作就是 immediateExecuting
取反:
到这里所有初始化方法中的临时信号就介绍完了,在下一节中会继续介绍初始化方法中的其它高阶信号。
RACCommand 接口中的高阶信号
每一个 RACCommand
对象中都管理着多个信号,它在接口中暴露出的四个信号是这一节关注的重点:
这一小节会按照顺序图中从上到下的顺序介绍 RACCommand
接口中暴露出来的信号,同时会涉及一些为了生成这些信号的中间产物。
executionSignals
executionSignals
是 RACCommand
中最重要的信号;从类型来看,它是一个包含信号的信号,在每次执行 -execute:
方法时,最终都会向 executionSignals
中传入一个最新的信号。
虽然它最重要,但是executionSignals
是这个几个高阶信号中实现最简单的:
_executionSignals = [[[self.addedExecutionSignalsSubject map:^(RACSignal *signal) { return [signal catchTo:[RACSignal empty]]; }] deliverOn:RACScheduler.mainThreadScheduler] setNameWithFormat:@"%@ -executionSignals", self];
它只是将信号中的所有的错误 NSError
转换成了 RACEmptySignal
对象,并派发到主线程上。
如果你只订阅了 executionSignals
,那么其实你不会收到任何的错误,所有的错误都会以 -sendNext:
的形式被发送到 errors
信号中,这会在后面详细介绍。
executing
executing
是一个表示当前是否有任务执行的信号,这个信号使用了在上一节中介绍的临时变量作为数据源:
_executing = [[[[[immediateExecuting deliverOn:RACScheduler.mainThreadScheduler] startWith:@NO] distinctUntilChanged] replayLast] setNameWithFormat:@"%@ -executing", self];
这里对 immediateExecuting
的变换还是非常容易理解的:
最后的 replayLast
方法将原有的信号变成了容量为 1
的 RACReplaySubject
对象,这样在每次有订阅者订阅 executing
信号时,都只会发送最新的状态,因为订阅者并不关心过去的 executing
的值。
enabled
enabled
信号流表示当前的命令是否可以再次被执行,也就是 -execute:
方法能否可以成功执行新的任务;该信号流依赖于另一个私有信号 immediateEnabled
:
RACSignal *enabledSignal = [RACSignal return:@YES]; _immediateEnabled = [[[[RACSignal combineLatest:@[ enabledSignal, moreExecutionsAllowed ]] and] takeUntil:self.rac_willDeallocSignal] replayLast];
虽然这个信号的实现比较简单,不过它同时与三个信号有关,enabledSignal
、moreExecutionsAllowed
以及 rac_willDeallocSignal
:
虽然图中没有体现出方法 -takeUntil:self.rac_willDeallocSignal
的执行,不过你需要知道,这个信号在当前 RACCommand
执行 dealloc
之后就不会再发出任何消息了。
而 enabled
信号其实与 immediateEnabled
相差无几:
_enabled = [[[[[self.immediateEnabled take:1] concat:[[self.immediateEnabled skip:1] deliverOn:RACScheduler.mainThreadScheduler]] distinctUntilChanged] replayLast] setNameWithFormat:@"%@ -enabled", self];
从名字你可以看出来,immediateEnabled
在每次原信号发送消息时都会重新计算,而 enabled
调用了 -distinctUntilChanged
方法,所以如果连续几次值相同就不会再次发送任何消息。
除了调用 -distinctUntilChanged
的区别之外,你可以看到 enabled
信号在最开始调用了 -take:
和 -concat:
方法:
[[self.immediateEnabled
take:1]
concat:[[self.immediateEnabled skip:1] deliverOn:RACScheduler.mainThreadScheduler]]
虽然序列并没有任何的变化,但是在这种情况下,enabled
信号流中的第一个值会在订阅线程上到达,剩下的所有的值都会在主线程上派发;如果你知道,在一般情况下,我们都会使用 enabled
信号来控制 UI 的改变(例如 UIButton
),相信你就会明白这么做的理由了。
errors
错误信号是 RACCommand
中比较简单的信号;为了保证 RACCommand
对此执行 -execute:
方法也可以继续运行,我们只能将所有的错误以其它的形式发送到 errors
信号中,防止向 executionSignals
发送错误信号后,executionSignals
信号就会中止的问题。
我们使用如下的方式创建 errors
信号:
RACMulticastConnection *errorsConnection = [[[self.addedExecutionSignalsSubject flattenMap:^(RACSignal *signal) { return [[signal ignoreValues] catch:^(NSError *error) { return [RACSignal return:error]; }]; }] deliverOn:RACScheduler.mainThreadScheduler] publish]; _errors = [errorsConnection.signal setNameWithFormat:@"%@ -errors", self]; [errorsConnection connect];
信号的创建过程是把所有的错误消息重新打包成RACErrorSignal
并在主线程上进行派发:
使用者只需要调用 -subscribeNext:
就可以从这个信号中获取所有执行过程中发生的错误。
RACCommand 的使用
RACCommand
非常适合封装网络请求,我们可以使用下面的代码封装一个网络请求:
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) { return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { NSURL *url = [NSURL URLWithString:@"http://localhost:3000"]; AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url]; NSString *URLString = [NSString stringWithFormat:@"/api/products/%@", input ?: @1]; NSURLSessionDataTask *task = [manager GET:URLString parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { [subscriber sendNext:responseObject]; [subscriber sendCompleted]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { [subscriber sendError:error]; }]; return [RACDisposable disposableWithBlock:^{ [task cancel]; }]; }]; }];
上面的 RACCommand
对象可以通过 -execute:
方法执行,同时,订阅 executionSignals
以及 errors
来获取网络请求的结果。
[[command.executionSignals switchToLatest] subscribeNext:^(id _Nullable x) { NSLog(@"%@", x); }]; [command.errors subscribeNext:^(NSError * _Nullable x) { NSLog(@"%@", x); }]; [command execute:@1];
向方法 -execute:
中传入了 @1
对象,从服务器中获取了 id = 1
的商品对象;当然,我们也可以传入不同的 id
来获取不同的模型,所有的网络请求以及 JSON 转换模型的逻辑都可以封装到这个 RACCommand
的 block 中,外界只是传入一个 id
,最后就从 executionSignals
信号中获取了开箱即用的对象。
总结
使用 RACCommand
能够优雅地将包含副作用的操作和与副作用无关的操作分隔起来;整个 RACCommand
相当于一个黑箱,从 -execute:
方法中获得输入,最后以向信号发送消息的方式,向订阅者推送结果。
这种执行任务的方式就像是一个函数,根据输入的不同,有着不同的输出,非常适合与 UI、网络操作的相关的任务,这也是 RACCommand
的设计的优雅之处。
References
Github Repo:iOS-Source-Code-Analyze
Follow: Draveness · GitHub
Source: http://draveness.me/raccommand
References
Github Repo:iOS-Source-Code-Analyze
Follow: Draveness · GitHub
Source: http://draveness.me/raccommand
链接:https://www.jianshu.com/p/ae71313f5846
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。