iOS-ReactiveCocoa使用之RACCommand

96
Tangentw
2016.12.10 09:37* 字数 2032

前言

前几天开始研究Cocoa的第三方编程框架ReactiveCocoa,其使用响应式、函数式的编程思想,对于初识者来说较为抽象,从RACSignalRACCommand,我花了不少时间去搞懂它们如何使用。其中,花费我最多时间去掌握的就是RACCommand,这货虽然刚开始难以理解难以使用,但是,当我初步了解其特性与应用后,我才发现了它是如此的强大。
下面就我对RACCommand的理解,来阐述它的基本介绍以及相关使用方法。

初识 RACCommand

创建 RACCommand

RACCommand的创建有两种形式:

- (id)initWithSignalBlock:(RACSignal * (^)(id input))signalBlock;  ①
- (id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock;  ②

第一种就是直接通过传进一个用于构建RACSignalblock参数来初始化RACCommand,而block中的参数input为执行command时传入的数据,另外,创建出的signal可在里面完成一些数据操作,如网络请求,本地数据库读写等等,而第二种则另外还需要传进一个能传递BOOL事件的RACSignal,这个signal的作用相当于过滤,当传递的布尔事件为真值时,command能够执行,反之则不行。

注意: 伴随着command一起构建的signal,记得要在操作完成后发送完成消息以表示其执行完了:

[subscriber sendCompleted];

否则不能再执行此command。

UIButton中有属性rac_command用于绑定一个已经创建好的command(其使用在后面讲到),当你使用第二种方式创建command时,button的enable属性会随command的可执行性而改变,意思是当传递布尔事件的信号传递了真值事件,按钮才可使用。另外,当你按下按钮,command开始执行时,按钮的enable被自动设置成了NO,除非command执行完了,怎么判断command执行完成了呢?就是当其伴随的signal发送完成事件的时候(上面提及到)。

注意: 当button的rac_command已经绑定了某个command,而这个command又是以第二种方式初始化,那么你就不能动态改变button的enable,如:

RAC(self.button, enable) = someSignal;

这样子运行起来会报错。(自己曾踩过的坑)

执行 RACCommand

RACCommand的执行使用下面的这个函数:

- (RACSignal *)execute:(id)input;

在上面已经提及到,input会作为创建command时其内部signal的构建block中的参数,用于传递数据。

订阅 RACCommand

订阅RACCommand我们可以使用其内部的属性executionSignals返回一个信号,然后对这个信号进行订阅。

[[aCommand executionSignals]
    subscribeNext:^(id x) {
        NSLog(@"%@",x);
    }];

在订阅的block中,我们打印了传递事件x的描述,最后会发现x原来是一个RACSignal,原因是RACCommand中的executionSignals属性是一个包裹着信号的信号,其包裹着的信号就是我们当初在创建RACCommand时进行构建的信号,所以当我们开始执行RACCommand时,executionSignals信号就会立即发送事件,传递出其包裹的信号,我们可以对这个信号进行订阅:

[[aCommand executionSignals]
    subscribeNext:^(RACSignal *x) {
        [x subscribeNext:^(id x) {
            //  Do something...
        }];
    }];

如果你嫌订阅两个事件麻烦的话,可以使用函数switchToLatest进行转换:

[[[aCommand executionSignals]switchToLatest]
    subscribeNext:^(id x) {
        //  Do something...
    }];

这样就比上面少写了一步信号订阅。

如果你想在RACCommand执行时做某些提示操作(弹出等待框,出现转来转去的菊花),并在执行后取消提示,你可以这样写:

[[aCommand executionSignals]
    subscribeNext:^(RACSignal *x) {
        //  开始提示
        [x subscribeNext:^(id x) {
            //  关闭提示
            //  Do something...
        }];
    }];

在对command进行错误处理的时候,我们不应该使用subscribeError:对command的executionSignals进行错误的订阅,因为executionSignals这个信号是不会发送error事件的,那当command包裹的信号发送error事件时,我们要怎样去订阅它呢?这里用到command的一个属性:errors,我们可以这样来对错误进行订阅:

[aCommand.errors
    subscribeNext:^(NSError *x) {
        NSLog(@"ERROR! --> %@",x);
}];

与 RACSubject的区别

虽然ReactiveCocoa的官方说过RACSubject较为灵活,所以建议少用,而我平时会经常使用RACSubject用其代替delegate。在刚开始接触RAC的时候,我会觉得RACCommandRACSubject非常相似,都能够控制执行,都能够进行订阅,然而,它们的区别也是挺大的。

举个栗子吧,用计算机网络中的术语,RACSubject更像“单工”,而RACCommand就类似于“半双工”。

  • RACSubject只能单向发送事件,发送者将事件发送出去让接收者接收事件后进行处理,所以,RACSubject可代替代理,被监听者可利用subject发送事件,监听者接收事件然后进行相应的监听处理,不过,事件的传递方向是单向的。

  • 对于RACCommand,我觉得用HTTP请求能够更形象地说明其原理,HTTP请求是由请求者向服务器发送一条网络请求,而服务器接收到请求然后经过相应处理后再向请求者返回处理过后的结果,数据流是双向的,RACCommand正是如此,让我想让某个部件进行某种会产生结果的操作时,利用RACCommand向此部件发送执行事件,部件接收到执行事件后进行相应操作处理并也通过RACCommand将操作结果回调到上层,使得事件得以双向流通。

    以上的解释是建立在RACCommand的事件产生与接收者为同一个对象的前提下的,而RACCommand也能将事件产生者和订阅者分离,让某个对象专门发送事件,通过RACCommand将事件传递到对数据进行操作处理的对象,最后,当数据处理完后再搭载着RACCommand把结果事件传出来,并被订阅者对象订阅。

下面的这张图表明了我对RACSubjectRACCommand的理解:

RACCommand 实战

讲解RAC最好的Demo就是Login(登录)界面的构建了,下面我们就来完成一个登录界面,主要使用RACCommand以及MVVM设计模式。

给出的需求:

  • 当所输入的用户名和密码字符串长度都大于6时,登录按钮才可用
  • 按下登录按钮后,显示表示处理中的旋转小菊花
  • 模拟网络环境,等待3秒后登录完毕,小菊花消失,并打出“登录成功”的Log

码代码:

  1. 用Storyboard把界面搭好,调整好布局,连好线,然后把菊花视图隐藏,并创建ViewModel:

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UITextField *userNameTF;
@property (weak, nonatomic) IBOutlet UITextField *passwordTF;
@property (weak, nonatomic) IBOutlet UIButton *loginBtn;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *juhuaView;
@property (strong, nonatomic) TanLoginViewModel *viewModel;
@end

 @implementation ViewController
- (void)viewDidLoad {
        [super viewDidLoad];
        self.juhuaView.hidden = YES;
        _viewModel = [[TanLoginViewModel alloc]init];
    
    }
@end
  1. 模拟网络请求,创建Networker,其包含网络请求的方法,在这方法返回带有登录完成事件的信号:
    @interface TanNetworker : NSObject
    + (RACSignal *)loginWithUserName:(NSString *) name password:(NSString *)password;
    @end
    
    @implementation TanNetworker
+ (RACSignal *)loginWithUserName:(NSString *) name password:(NSString *)password
{
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [subscriber sendNext:[NSString stringWithFormat:@"User %@, password %@, login!",name, password]];
                [subscriber sendCompleted];
        });
            return nil;
    }];
}
@end
  1. 定义登录视图的ViewModel,在里面创建登录的command:
    @interface TanLoginViewModel : NSObject
    @property(nonatomic, copy) NSString *userName;
    @property(nonatomic, copy) NSString *password;
    @property(nonatomic, strong, readonly) RACCommand   *loginCommand;
    @end
    
    @implementation TanLoginViewModel
- (instancetype)init
{
        if (self = [super init]) {
            RACSignal *userNameLengthSig = [RACObserve(self, userName)
                                            map:^id(NSString *value) {
                                                if (value.length > 6) return @(YES);
                                                return @(NO);
                                            }];
            RACSignal *passwordLengthSig = [RACObserve(self, password)
                                            map:^id(NSString *value) {
                                                if (value.length > 6) return @(YES);
                                                return @(NO);
                                            }];
            RACSignal *loginBtnEnable = [RACSignal combineLatest:@[userNameLengthSig, passwordLengthSig] reduce:^id(NSNumber *userName, NSNumber *password){
                return @([userName boolValue] && [password boolValue]);
            }];
        
        
            _loginCommand = [[RACCommand alloc]initWithEnabled:loginBtnEnable signalBlock:^RACSignal *(id input) {
                return [TanNetworker loginWithUserName:self.userName password:self.password];
            }];
        }
        return self;
}
@end
  1. 在控制器中实现RAC,并且订阅command,响应事件:
    @weakify(self)
    RAC(self.viewModel, userName) = self.userNameTF.rac_textSignal;
    RAC(self.viewModel, password) = self.passwordTF.rac_textSignal;
    self.loginBtn.rac_command = self.viewModel.loginCommand;
    [[self.viewModel.loginCommand executionSignals]
    subscribeNext:^(RACSignal *x) {
        @strongify(self)
        self.juhuaView.hidden = NO;
        [x subscribeNext:^(NSString *x) {
            self.juhuaView.hidden = YES;
            NSLog(@"%@",x);
        }];
    }];

到这里,一个利用RACCommandMVVM设计模式进行登录操作的小Demo就完成了~

跑起来

下面就让我们来测试一下这个小Demo

  • 运行程序,一开始你会看到登录按钮标题颜色为灰色,表明当前登录按钮不可用,当我输入的用户名或密码其中一个的字符串长度小于或等于6的时候,登录按钮也会保持不可用状态:

  • 一旦用户名跟密码的字符串长度都满足条件时,登录按钮就会改变颜色,表明可用:

  • 现在,点击登录按钮,菊花视图会立即显示出来,并且登录按钮会自动变成不可用的状态:

  • 登录完成,编译器打印Log,菊花视图隐藏,登录按钮恢复可用状态:

iOS开发
Web note ad 1