对 RAC 中 RACCommand 的理解和应用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/kmyhy/article/details/81487049

RACSignal 和 RACCommand

RACCommand 是 RAC 中的最复杂的一个类之一,它也是一种广义上的信号。RAC 中信号其实是一种对象(或者是不同代码块)之间通信机制,在面向对象中,类之间的通信方式主要是方法调用,而信号也是一种调用,只不过它是函数式的,因此信号不仅仅可以在对象之间相互调用(传参),也可以在不同代码块(block)之间进行调用。

一般来说,RAC 中用 RACSignal 来代表信号。一个对象创建 RACSignal 信号,创建信号时会包含一个 block,这个 block 的作用是发送信号给订阅者(类似方法返回值或回调函数)。另一个对象(或同一个对象)可以用这个信号进行订阅,从而获得发送者发送的数据。这个过程和方法调用一样,信号相当于暴露给其它对象的方法,订阅者订阅信号相当于调用信号中的方法(block),只不过返回值的获得变成了通过 block 来获得。此外,你无法直接向 RACSignal 传递参数,要向信号传递参数,需要提供一个方法,将要传递的参数作为方法参数,创建一个信号,通过 block 的捕获局部变量方式将参数捕获到信号的 block 中。

而 RACCommand 不同,RACCommand 的订阅不使用 subscribeNext 方法而是用 execute: 方法。而且 RACCommand 可以在订阅/执行(即 excute:方法)时传递参数。因此当需要向信号传递参数的时候,RACComand 更好用。

此外,RACCommand 包含了一个 executionSignal 的信号,这个信号是对用户透明的,它是自动创建的,由 RACCommand 进行管理。许多资料中把它称之为信号中的信号,是因为这个信号会发送其它信号——即 RACCommand 在初始化的 signalBlock 中创建(return)的信号。这个信号是 RACCommand 创建时由我们创建的,一般是用于处理一些异步操作,比如网络请求等。请看代码:

    @weakify(self);
    RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(NSString* input) {

            @strongify(self);

            NSDictionary *body = @{@"memberCode": input};

            // 进行网络操作,同时将这个操作封装成信号 return
            return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
                // 一些网络操作
                ...
                return nil;
            }];
        }];

在这段代码中,需要注意:

  1. initWithSignalBlock 方法初始化一个 RACCommand,这个方法需要提供一个 signalBlock 块参数。
  2. signalBlock 块的签名中,有一个入参 input,它是订阅者在订阅/执行(调用 RACCommand 的 execute: 方法)时传入的。可以是任意类型(id),这里我们为了简单起见,定义为 NSString(它真的就是一个 NSString),从而减少类型转换的代码。
  3. signalBlock 块的返回值是一个信号,因此在块体中,我们用 createSignal 创建了一个信号作为返回值。这是必须的,因为这个信号中定义了一些我们需要进行的处理,比如网络请求等。真正的任务是在这个信号中进行的,外部的订阅者通过 execute: 方法订阅/执行这个 RACCommand 时,这些代码就得以执行。这些代码的具体内容在这里并不重要,请忽略。

从上面来看,其实用一个简单的 RACSignal 也能完成同样的工作。那为什么还要用 RACCommand 呢?

这就是 RACCommand 的另一个优点了,它可以监听 RACCommand 自身的执行状态,比如开始、进行中、完成、错误等。用 RACSignal 可以监听到完成(complete)、错误(error)、进行中(next)。但开始就无法实现了,而且实现起来代码比较分散和难看(吐槽一下,RAC 绝大多数时候并没有为我们提供新功能,只不过是一种代码美学的处理而已)。

RACCommand 的解决办法很简单,就是用一个信号来监听另一个信号的执行。也就是 executionSignal 信号的来由。在本文中,我们会叫他外层信号。而 signalBlock 中的那个信号(真正执行主要工作的)则叫内层信号。

理论足够了,来做一些实验吧。

开始

新建一个 Single View 项目。UI 很简单,就一个 ViewController 类,上面包含了一个 UIButton 和 UITextView,随便你怎么布局它们。然后创建相应的连接。

重要的是 ViewModel 类,我们用它简单实现一个 MVVM 架构。在 ViewModel.h 里声明一些属性:

@property(nonatomic, strong) RACCommand *requestData;
@property(nonatomic, assign) HTTPRequestStatus requestStatus;

@property (strong, nonatomic) NSDictionary *data;
@property (strong, nonatomic) NSError* error;
  1. requestData 就是本文的核心了,一个 RACCommand 类,提供一些信号给 controller 用于更新 UI(比如小菊花)。
  2. requestStatus 记录网络请求的状态,比如开始、完成、出错等,它是一个枚举,定义如下:

    typedef NS_ENUM(NSUInteger, HTTPRequestStatus) {
    HTTPRequestStatusBegin,
    HTTPRequestStatusEnd,
    HTTPRequestStatusError,
    };
  3. data 用于保存成功请求后获得的数据。
  4. error 用于保存请求失败后的错误。

接下来是实现了,首先看 RACCommand 的创建。

创建 RACCommand

在 ViewModel.m 中,我们通过懒加载方式来初始化 RACCommand 对象:

- (RACCommand *)requestData {
    if (!_requestData) {
        @weakify(self);
        _requestData = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(NSString* input) {
            @strongify(self);
            NSDictionary *body = @{@"memberCode": input};
            // 进行网络操作,同时将这个操作封装成信号 return
            return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
                [self postUrl:kSubscribeURL params:body requestType:@"json" success:^(id  _Nullable responseObject) {
                    [subscriber sendNext:responseObject];
                    [subscriber sendCompleted];
                } failure:^(NSError *error) {
                    [subscriber sendError:error];
                }];

                return nil;
            }];
        }];
    }
    return _requestData;
}

这段代码大部分都在前面讲过了,只有 [self postUrl: … 这部分代码是新的。这部分代码是一个将 HTTP 请求封装为 RACSignal 的典型示例,相信每个人都不陌生,你可以替换成自己的代码。

订阅信号

RACCommand 中封装了各种信号,我们只用到了外层信号(executionSignal)和内层信号。订阅这些信号能够让我们实现两个目的:拿到请求返回的数据、跟踪 RACCommand 开始结束状态。定义一个方法来做这些事情:

- (void)subcribeCommandSignals {
    @weakify(self)
        // 1. 订阅外层信号
    [self.requestData.executionSignals subscribeNext:^(RACSignal* innerSignal) {
        @strongify(self)
        // 2. 订阅内层信号
        [innerSignal subscribeNext:^(NSDictionary* x) {
            self.data = x;
            self.requestStatus = HTTPRequestStatusEnd;
        }];

        self.error = nil;
        self.requestStatus = HTTPRequestStatusBegin;
    }];
    // 3. 订阅 errors 信号
    [self.requestData.errors subscribeNext:^(NSError * _Nullable x) {
        @strongify(self)
        self.error = x;
        self.data = nil;
        self.requestStatus = HTTPRequestStatusError; // 这一句必须放在最后一句,否者 controller 拿不到数据
    }];
}

这里需要注意:

  1. 订阅外层信号(即 executionSignals)。外层信号在订阅或执行(即 execute: )时发送。因此我们可以将它视作请求即将开始之前的信号,在这里将 self.error 清空,将 requestStatus 修改为 begin。
  2. 订阅内层信号,因为内层信号由外层信号(executionSignals)作为数据发送(sendNext:),而发送的数据一般是作为 subcribeNext:时的 block 的参数来接收的,因此在这个块中,块的参数就是内层信号。这样我们就可以订阅内层信号了,同时获取数据(保存到 data 属性)并修改 requestStatus 为 end。
  3. RACCommand 比较特殊的一点是 error 信号需要在 errors 中订阅,而不能在 executionSignals 中订阅。在这里我们订阅了 errors 信号,并修改 data、error 和 requestStatus 属性值。

最后,在 init 方法中调用这个方法,来完成对相关信号的订阅。

- (id)init {
    self = [super init];
    if (self) {
        [self subcribeCommandSignals];
    }
    return self;
}

信号部分处理完了,接下来是 UI。

Controller

UI 需要关心 RACCommand 的开始、完成、失败状态,以便显示隐藏小菊花,同时 UI 需要关心 RACCommand 获取的数据并做展示(这里为了简单起见,直接用 Text View 显示出数据)。这其实是对 ViewModel 中的 data 属性和 requestStatus 属性的监听,因此,接下来的一步就是在 controller 中将 View 和 ViewModel 进行绑定了。绑定的代码如下:

-(void)bindViewModel{
@weakify(self)
        // 1. 
    [[RACObserve(_viewModel, requestStatus) skip:1] subscribeNext:^(NSNumber* x) {
        @strongify(self)
        switch ([x intValue]) {
                case HTTPRequestStatusBegin:
                [MBProgressHUD showHUDAddedTo:self.view animated:YES];
                break;
                case HTTPRequestStatusEnd:
                [MBProgressHUD hideHUDForView:self.view animated:YES];
                break;
                case HTTPRequestStatusError:
                [MBProgressHUD hideHUDForView:self.view animated:YES];
                [MBProgressHUD showError:self.viewModel.error.localizedDescription toView:self.view];
                break;
         }
    }];

    // 2. 
    RAC(self.textView,text) = [[RACObserve(_viewModel, data) skip:1] map:^id _Nullable(NSDictionary* value) {
        return dic2str(value);
    }];

    // 3.
//    _button.rac_command = _viewModel.requestData;
    [[_button rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
        @strongify(self)
        [self.viewModel.requestData execute:@"96671e1a812e46dfa4264b9b39f3e225"];
    }];

}
  1. 监听 ViewModel 的 requestStatus 属性,当属性为 begin 时显示小菊花,当属性为 end 时隐藏小菊花,当属性为 error 时隐藏小菊花并显示错误消息。这里需要注意的是,RAC 在第一次绑定时会自动发送一条信号,这时 requestStatus 的初始值是默认值 0,这样的消息显然是多余的,我们要用 skip:1 过滤掉。
  2. 将 ViewModel 的 data 属性和 textView 进行绑定。因为 data 是一个 NSDictionary,而 textView 的 text 属性是 NSString,显然无法做这样的绑定,于是我们用 map: 方法把 data 从 NSDictionary 转换为 NSString。这里的 dic2str 便利函数替我们完成这个工作。同样 RACObserve 会在第一次绑定时发送一条多余信号,我们用 skip:1 过滤掉。
  3. 订阅按钮的 rac 信号进行事件处理。这里没有使用 _button.rac_command = _viewModel.requestData 这样的方式,虽然它看起来比较简单,但却无法在调用 RACCommand 时传递参数。因此我们手工订阅了按钮的 rac 信号,并在订阅块中手动调用了 execute: 方法,以此来传递了一个字符串参数。作为演示,这里的参数传递的是一个常量,你可以传入任意值(id类型)。

最后,在 viewDidLoad 方法中,我们需要初始化 ViewModel,并调用 bindViewModel 方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    _viewModel = [ViewModel new];

    [self bindViewModel];
}

注: 这里使用的 RAC 是 ReactiveObjC 3.0,如果你使用了 ReactiveCocoa 的其它版本,有的代码可能需要修改。

猜你喜欢

转载自blog.csdn.net/kmyhy/article/details/81487049