ReactiveCocoa of elegant RACCommand

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 RACSignaland RACSubjectbe able to solve most of the problems, but RACCommanduse can bring great convenience for us, especially in operations related to the side effects.

 
What-is-RACCommand

The article will not discuss the RACCommandparallel execution problems, which is ignored allowsConcurrentExecutionand allowsConcurrentExecutionSubjectthe presence, but they are indeed RACCommandvery important in here just to reduce unnecessary interference factors.

RACCommand Profile

Described in the previous articles in RACSignaldifferent other elements, RACCommandit does not mean that the data stream, it's just an inheritance from NSObjectthe 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 RACStreamor RACSignalsubclass, but a management for RACSignalthe creation and subscription class.

In ReactiveCocoa of the portion FrameworkOverview RACCommandthis 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 RACCommandcan 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 RACCommandwill pass in a type of initialization RACSignal<ValueType> * (^)(InputType _Nullable input)of signalBlock:

- (instancetype)initWithSignalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;

Input for the InputTypereturn value RACSignal<ValueType> *, and InputTypethat is to call -execute:the incoming object when the method:

- (RACSignal<ValueType> *)execute:(nullable InputType)input;

This is RACCommandan external variable (or "side-effects") passed to the method of internal ReactiveCocoa, you can be understood as RACCommandan external variable InputTypeconverted to use RACSignalwrapped ValueTypeobject.

 
Execute-For-RACCommand

Take the following code as an example, first look at RACCommandhow 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 RACCommandobject, 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

-switchToLatestThe method of operation can only signal a .

Every time executionSignalswhen sending a new signal, the switchToLatestmethod 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.

 
Multiple-Executes

In the code above have one last question to be answered, why use RACScheduler.mainThreadSchedulerafter a delayed call -execute:method? Because by default RACCommanddo 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.errorsget.

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'

RACCommandThe most important internal "signal" is addedExecutionSignalsSubject:

@property (nonatomic, strong, readonly) RACSubject *addedExecutionSignalsSubject;

This RACSubjectobject is derived by a variety of operating almost all RACCommandthe other signals, we will specifically described in the next section;

Since it addedExecutionSignalsSubjectis 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 immediateEnabledthe reading whether the execution of the current command's BOOLvalue, if not implemented, directly return RACErrorSignal.

 
Execute-on-RACCommand

-execute:The method is the only one for the addedExecutionSignalsSubjectmethod of production information.

In execution signalBlockreturns RACSignalafter, it will be packaged into a current signal RACMulticastConnection, and then call the -sendNext:method to send to addedExecutionSignalsSubjectthe Executive -connectway 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;
}

这一小节并不能完全介绍全部的七个信号的实现,只会介绍其中的 immediateExecutingmoreExecutionsAllowed 两个临时信号,剩下的信号都会在下一节中分析。

表示当前有操作执行的信号

首先是 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 如下:

 
immediateExecuting-Signal-in-RACCommand
  1. -catchTo: 将所有的错误转换成 RACEmptySignal 信号;
  2. -flattenMap: 将每一个信号的开始和结束的时间点转换成 1-1 两个信号;
  3. -scanWithStart:reduce:0 开始累加原有的信号;
  4. -map: 将大于 1 的信号转换为 @YES
  5. -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 取反:

 
MoreExecutionAllowed-Signa

到这里所有初始化方法中的临时信号就介绍完了,在下一节中会继续介绍初始化方法中的其它高阶信号。

RACCommand 接口中的高阶信号

每一个 RACCommand 对象中都管理着多个信号,它在接口中暴露出的四个信号是这一节关注的重点:

 
RACCommand-Interface

这一小节会按照顺序图中从上到下的顺序介绍 RACCommand 接口中暴露出来的信号,同时会涉及一些为了生成这些信号的中间产物。

executionSignals

executionSignalsRACCommand 中最重要的信号;从类型来看,它是一个包含信号的信号,在每次执行 -execute: 方法时,最终都会向 executionSignals 中传入一个最新的信号。

虽然它最重要,但是executionSignals 是这个几个高阶信号中实现最简单的:

_executionSignals = [[[self.addedExecutionSignalsSubject
    map:^(RACSignal *signal) {
        return [signal catchTo:[RACSignal empty]];
    }]
    deliverOn:RACScheduler.mainThreadScheduler]
    setNameWithFormat:@"%@ -executionSignals", self];

它只是将信号中的所有的错误 NSError 转换成了 RACEmptySignal 对象,并派发到主线程上。

 
Execution-Signals

如果你只订阅了 executionSignals,那么其实你不会收到任何的错误,所有的错误都会以 -sendNext: 的形式被发送到 errors 信号中,这会在后面详细介绍。

executing

executing 是一个表示当前是否有任务执行的信号,这个信号使用了在上一节中介绍的临时变量作为数据源:

_executing = [[[[[immediateExecuting
    deliverOn:RACScheduler.mainThreadScheduler]
    startWith:@NO]
    distinctUntilChanged]
    replayLast]
    setNameWithFormat:@"%@ -executing", self];

 

这里对 immediateExecuting 的变换还是非常容易理解的:

 
Executing-Signa

最后的 replayLast 方法将原有的信号变成了容量为 1RACReplaySubject 对象,这样在每次有订阅者订阅 executing 信号时,都只会发送最新的状态,因为订阅者并不关心过去的 executing 的值。

enabled

enabled 信号流表示当前的命令是否可以再次被执行,也就是 -execute: 方法能否可以成功执行新的任务;该信号流依赖于另一个私有信号 immediateEnabled

RACSignal *enabledSignal = [RACSignal return:@YES];

_immediateEnabled = [[[[RACSignal
    combineLatest:@[ enabledSignal, moreExecutionsAllowed ]]
    and]
    takeUntil:self.rac_willDeallocSignal]
    replayLast];

虽然这个信号的实现比较简单,不过它同时与三个信号有关,enabledSignalmoreExecutionsAllowed 以及 rac_willDeallocSignal

 
Immediate-Enabled-Signa

虽然图中没有体现出方法 -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 并在主线程上进行派发:
 
Errors-Signals

使用者只需要调用 -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: 方法中获得输入,最后以向信号发送消息的方式,向订阅者推送结果。

 
RACCommand-Side-Effect

这种执行任务的方式就像是一个函数,根据输入的不同,有着不同的输出,非常适合与 UI、网络操作的相关的任务,这也是 RACCommand 的设计的优雅之处。

References

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · GitHub

Source: http://draveness.me/raccommand

作者:Draveness
链接:https://www.jianshu.com/p/ae71313f5846
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

 

Guess you like

Origin www.cnblogs.com/xujinzhong/p/11325419.html