iOS-ReactiveCocoa使用之RACCommand

前言

前几天开始研究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,菊花视图隐藏,登录按钮恢复可用状态:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容