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 创建时由我们创建的,一般是用于处理一些异步操作,比如网络请求等。
我们还是通过一个实例来说说RACCommand的用法吧。
开始
这个例子我们准备用MVVM架构来实现,虽然只是个简单的例子。我们新建一个view model类CommandViewModal,其包含如下属性
@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
在 CommandViewModal.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 _Nonnull subscriber) {
[self postUrl:testUrl params:body requestType:@"json" success:^(id _Nullable responseObject) {
[subscriber sendNext:responseObject];
[subscriber sendCompleted];
} failure:^(NSError *error) {
[subscriber sendError:error];
}];
return nil;
}];
}];
}
return _requestData;
}
在这段代码中,需要注意:
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 中的那个信号(真正执行主要工作的)则叫内层信号。
订阅信号
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
在我们的ViewController上添加一个UIButton 和 UITextView,随便你怎么布局它们。然后创建相应的连接。
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;
[[_btn 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];
// Do any additional setup after loading the view.
_viewModel = [CommandViewModal new];
[self bindViewModel];
}
Demo地址:https://github.com/BigFish2018/MVVM.git
参考:
https://blog.csdn.net/kmyhy/article/details/81487049