ReactiveCocoa用法帮助

ReactiveCocoa框架概览

网上有个说法对有个ReactiveCocoa很好的理解:

可以把信号想象成水龙头,只不过里面不是水,而是玻璃球(value),直径跟水管的内径一样,这样就能保证玻璃球是依次排列,不会出现并排的情况(数据都是线性处理的,不会出现并发情况)。水龙头的开关默认是关的,除非有了接收方(subscriber),才会打开。这样只要有新的玻璃球进来,就会自动传送给接收方。可以在水龙头上加一个过滤嘴(filter),不符合的不让通过,也可以加一个改动装置,把球改变成符合自己的需求(map)。也可以把多个水龙头合并成一个新的水龙头(combineLatest:reduce:),这样只要其中的一个水龙头有玻璃球出来,这个新合并的水龙头就会得到这个球。

下面我来逐一介绍ReactiveCocoa框架的每个组件

Streams

Streams 表现为RACStream类,可以看做是水管里面流动的一系列玻璃球,它们有顺序的依次通过,在第一个玻璃球没有到达之前,你没法获得第二个玻璃球。
RACStream描述的就是这种线性流动玻璃球的形态,比较抽象,它本身的使用意义并不很大,一般会以signals或者sequences等这些更高层次的表现形态代替。

Signals

Signals 表现为RACSignal类,就是前面提到水龙头,ReactiveCocoa的核心概念就是Signal,它一般表示未来要到达的值,想象玻璃球一个个从水龙头里出来,只有了接收方(subscriber)才能获取到这些玻璃球(value)。

Signal会发送下面三种事件给它的接受方(subscriber),想象成水龙头有个指示灯来汇报它的工作状态,接受方通过-subscribeNext:error:completed:对不同事件作出相应反应

  • next 从水龙头里流出的新玻璃球(value)
  • error 获取新的玻璃球发生了错误,一般要发送一个NSError对象,表明哪里错了
  • completed 全部玻璃球已经顺利抵达,没有更多的玻璃球加入了

一个生命周期的Signal可以发送任意多个“next”事件,和一个“error”或者“completed”事件(当然“error”和“completed”只可能出现一种)

Subjects

subjects 表现为RACSubject类,可以认为是“可变的(mutable)”信号/自定义信号,它是嫁接非RAC代码到Signals世界的桥梁,很有用。嗯。。。 这样讲还是很抽象,举个例子吧:

RACSubject *letters = [RACSubject subject];
RACSignal *signal = [letters sendNext:@"a"];

可以看到@"a"只是一个NSString对象,要想在水管里顺利流动,就要借RACSubject的力。

Commands

与RACSignal 等元素不同,RACCommand 并不表示数据流,它只是一个继承自 NSObject 的类,但是它却可以用来创建和订阅用于响应某些事件的信号。

@interface RACCommand<__contravariant InputType, __covariant ValueType> : NSObject

@end

它本身并不是一个 RACStream 或者 RACSignal 的子类,而是一个用于管理 RACSignal 的创建与订阅的类。

在 ReactiveCocoa 中的 FrameworkOverview 部分对 RACCommand 有这样的解释:

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.

在用于与 UIKit 组件进行交互或者执行包含副作用的操作时,RACCommand 能够帮助我们更快的处理并且响应任务,减少编码以及工程的复杂度。

直接举个例子吧,比如一个简单的注册界面:

RACSignal *formValid=[RACSignal
    combineLatest:@[
        self.userNameField.rac_textSignal,
        self.emailField.rac_textSignal,
    ]
    reduce:^(NSString *userName,NSString *email){
        return@(userName.length>0
                &&email.length>0);
    }];

RACCommand *createAccountCommand = [RACCommand commandWithCanExecuteSignal:formValid];
RACSignal *networkResults=[[[createAccountCommand
  addSignalBlock:^RACSignal *(idvalue){
      //... 网络交互代码
}] switchToLatest] deliverOn:[RACSchedulermainThreadScheduler]];

// 绑定创建按钮的 UI state 和点击事件
[[self.createButtonrac_signalForControlEvents:UIControlEventTouchUpInside]executeCommand:createAccountCommand];
Sequences

sequence 表现为RACSequence类,可以简单看做是RAC世界的NSArray,RAC增加了-rac_sequence方法,可以使诸如NSArray这些集合类(collection classes)直接转换为RACSequence来使用。

Schedulers

scheduler 表现为RACScheduler类,类似于GCD;

but schedulers support cancellationbut schedulers support cancellation, and always execute serially.

ReactiveCocoa的简单使用

实践出真知,下面就举一些简单的例子,一起看看RAC的使用

Subscription

接收 -subscribeNext: -subscribeError: -subscribeCompleted:

RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

// 依次输出 A B C D…
[letters subscribeNext:^(NSString *x) {
    NSLog(@"%@", x);
}];
Injecting effects

注入效果 -doNext: -doError: -doCompleted:,看下面注释应该就明白了:

__block unsigned subscriptions=0;

RACSignal *loggingSignal = [RACSignal createSignal:^RACDisposable *(id subscriber) {
    subscriptions++;
    [subscriber sendCompleted];
    returnnil;
}];

// 不会输出任何东西
loggingSignal=[loggingSignal doCompleted:^{
    NSLog(@"about to complete subscription %u",subscriptions);
}];

// 输出:
// about to complete subscription 1
// subscription 1
[loggingSignal subscribeCompleted:^{
    NSLog(@"subscription %u",subscriptions);
}];
Mapping

-map: 映射,可以看做对玻璃球的变换、重新组装

RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
 // Contains: AA BB CC DD EE FF GG HH II
RACSequence *mapped = [letters map:^(NSString *value) {
   return [value stringByAppendingString:value];
}];
Filtering

-filter:过滤,不符合要求的玻璃球不允许通过

RACSequence *numbers=[@"1 2 3 4 5 6 7 8 9"componentsSeparatedByString:@" "].rac_sequence;

// Contains: 2 4 6 8
RACSequence *filtered=[numbers filter:^BOOL(NSString *value){
    return(value.intValue%2)==0;
}];
Concatenating

-concat: 把一个水管拼接到另一个水管之后

RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *concatenated = [letters concat:numbers];
Flattening

-flatten: Sequences are concatenated

RACSequence *letters=[@"A B C D E F G H I"componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers=[@"1 2 3 4 5 6 7 8 9"componentsSeparatedByString:@" "].rac_sequence;
RACSequence *sequenceOfSequences = @[letters,numbers].rac_sequence;

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *flattened=[sequenceOfSequences flatten];

Signals are merged (merge可以理解成把几个水管的龙头合并成一个,哪个水管中的玻璃球哪个先到先吐哪个玻璃球)

RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *signalOfSignals = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    [subscriber sendNext:letters];
    [subscriber sendNext:numbers];
    [subscriber sendCompleted];
    return nil;
}];

RACSignal *flattened = [signalOfSignals flatten];

// Outputs: A 1 B C 2
[flattened subscribeNext:^(NSString *x) {
    NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];
Mapping and flattening

-flattenMap:先 map 再 flatten

RACSequence *numbers=[@"1 2 3 4 5 6 7 8 9"componentsSeparatedByString:@" "].rac_sequence;

// Contains: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
RACSequence *extended=[numbers flattenMap:^(NSString *num){
    return@[num,num].rac_sequence;
}];

// Contains: 1_ 3_ 5_ 7_ 9_
RACSequence *edited=[numbers flattenMap:^(NSString *num){
    if(num.intValue%2==0){
        return[RACSequenceempty];
    }else{
        NSString *newNum=[num stringByAppendingString:@"_"];
        return[RACSequencereturn:newNum];
    }
}];

RACSignal *letters=[@"A B C D E F G H I"componentsSeparatedByString:@" "].rac_sequence.signal;

[[letters flattenMap:^(NSString *letter){
    return[databasesaveEntriesForLetter:letter];
}] subscribeCompleted:^{
    NSLog(@"All database entries saved successfully.");
}];
Sequencing

-then: 会等待completed事件的发送,然后再订阅由then block返回的signal。这样就高效地把控制权从一个signal传递给下一个。

RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

// 新水龙头只包含: 1 2 3 4 5 6 7 8 9
//
// 但当有接收时,仍会执行旧水龙头doNext的内容,所以也会输出 A B C D E F G H I
RACSignal *sequenced = [[letters doNext:^(NSString *letter) {
    NSLog(@"%@", letter);
}] then:^{
    return [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence.signal;
}];
Merging

+merge: 前面在flatten中提到的水龙头的合并

RACSubject *letters=[RACSubject subject];
RACSubject *numbers=[RACSubject subject];
RACSignal *merged=[RACSignal merge:@[letters,numbers]];

// Outputs: A 1 B C 2
[merged subscribeNext:^(NSString *x){
    NSLog(@"%@",x);
}];

[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];  
Combining latest values

+combineLatest:任何时刻取每个水龙头吐出的最新的那个玻璃球

RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *combined = [RACSignal
combineLatest:@[ letters, numbers ]
reduce:^(NSString *letter, NSString *number) {
    return [letter stringByAppendingString:number];
}];

// Outputs: B1 B2 C2 C3
[combined subscribeNext:^(id x) {
    NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];
[numbers sendNext:@"2"];
[letters sendNext:@"C"];
[numbers sendNext:@"3"];
Switching

-switchToLatest:取指定的那个水龙头的吐出的最新玻璃球

RACSubject *letters=[RACSubject subject];
RACSubject *numbers=[RACSubject subject];
RACSubject *signalOfSignals=[RACSubject subject];

RACSignal *switched=[signalOfSignals switchToLatest];

// Outputs: A B 1 D
[switched subscribeNext:^(NSString *x){
    NSLog(@"%@",x);
}];

[signalOfSignals sendNext:letters];
[letters sendNext:@"A"];
[letters sendNext:@"B"];

[signalOfSignals sendNext:numbers];
[letters sendNext:@"C"];
[numbers sendNext:@"1"];

[signalOfSignals sendNext:letters];
[numbers sendNext:@"2"];
[letters sendNext:@"D"];
常用宏

RAC 可以看作某个属性的值与一些信号的联动

RAC(self.submitButton.enabled) = [RACSignal combineLatest:@[self.usernameField.rac_textSignal, self.passwordField.rac_textSignal]reduce:^id(NSString *userName, NSString *password) {
    return @(userName.length >= 6 && password.length >= 6);
}];
RACObserve 监听属性的改变,使用block的KVO
[RACObserve(self.textField,text)subscribeNext:^(NSString *newName){
    NSLog(@"%@",newName);
}];
UI Event

RAC为系统UI提供了很多category,非常棒,比如UITextView、UITextField文本框的改动rac_textSignal,UIButton的的按下rac_command等等。

常用的模式

map + switchToLatest

switchToLatest: 的作用是自动切换signal of signals到最后一个,比如的command.executionSignals就可以使用switchToLatest:

map:的作用很简单,对sendNextvalue做一下处理,返回一个新的值。

如果把这两个结合起来就有意思了,想象这么个场景,当用户在搜索框输入文字时,需要通过网络请求返回相应的hints,每当文字有变动时,需要取消上一次的请求,就可以使用这个配搭。简单演示一下:

NSArray *pins = @[@172230988, @172230947, @172230899, @172230777, @172230707]; 
__block NSInteger index = 0; 

RACSignal *signal = [[[[RACSignal interval:0.1 onScheduler:[RACScheduler scheduler]] 
                    take:pins.count] 
                    map:^id(id value) { 
                        return [[[HBAPIManager sharedManager] fetchPinWithPinID:[pins[index++] intValue]] doNext:^(id x) { 
                            NSLog(@"这里只会执行一次"); 
                        }]; 
                    }] 
                    switchToLatest]; 

[signal subscribeNext:^(HBPin *pin) { 
    NSLog(@"pinID:%d", pin.pinID); 
} completed:^{ 
    NSLog(@"completed"); 
}]; 

// output 
// 2014-06-05 17:40:49.851 这里只会执行一次 
// 2014-06-05 17:40:49.851 pinID:172230707 
// 2014-06-05 17:40:49.851 completed 
takeUntil

takeUntil:someSignal 的作用是当someSignal sendNext时,当前的signal就sendCompleted,someSignal就像一个拳击裁判,哨声响起就意味着比赛终止。

它的常用场景之一是处理cell的button的点击事件,比如点击Cell的详情按钮,需要push一个VC,就可以这样:

[[[cell.detailButton 
rac_signalForControlEvents:UIControlEventTouchUpInside] 
takeUntil:cell.rac_prepareForReuseSignal] 
subscribeNext:^(id x) { 
    // generate and push ViewController 
}]; 

如果不加takeUntil:cell.rac_prepareForReuseSignal,那么每次Cell被重用时,该button都会被addTarget:selector

替换Delegate

出现这种需求,通常是因为需要对Delegate的多个方法做统一的处理,这时就可以造一个signal出来,每次该Delegate的某些方法被触发时,该signal就会sendNext。

@implementation UISearchDisplayController (RAC) 

- (RACSignal *)rac_isActiveSignal { 
    self.delegate = self; 
    RACSignal *signal = objc_getAssociatedObject(self, _cmd); 
    if (signal != nil) return signal; 
 
    /* Create two signals and merge them */ 
    RACSignal *didBeginEditing = [[self rac_signalForSelector:@selector(searchDisplayControllerDidBeginSearch:)  
                                    fromProtocol:@protocol(UISearchDisplayDelegate)] mapReplace:@YES]; 
    RACSignal *didEndEditing = [[self rac_signalForSelector:@selector(searchDisplayControllerDidEndSearch:)  
                                  fromProtocol:@protocol(UISearchDisplayDelegate)] mapReplace:@NO]; 
    signal = [RACSignal merge:@[didBeginEditing, didEndEditing]]; 
 
 
    objc_setAssociatedObject(self, _cmd, signal, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
    return signal; 
} 
@end 
rac_signalForSelector

rac_signalForSelector: 这个方法会返回一个signal,当selector执行完时,会sendNext,也就是当某个方法调用完后再额外做一些事情。用在category会比较方便,因为Category重写父类的方法时,不能再通过[super XXX]来调用父类的方法,当然也可以手写Swizzle来实现,不过有了rac_signalForSelector:就方便多了。

rac_signalForSelector: fromProtocol:可以直接实现对protocol的某个方法的实现(听着有点别扭呢),比如,我们想实现UIScrollViewDelegate的某些方法,可以这么写:

[[self rac_signalForSelector:@selector(scrollViewDidEndDecelerating:) fromProtocol:@protocol(UIScrollViewDelegate)] subscribeNext:^(RACTuple *tuple) { 
// do something 
}]; 

[[self rac_signalForSelector:@selector(scrollViewDidScroll:) fromProtocol:@protocol(UIScrollViewDelegate)] subscribeNext:^(RACTuple *tuple) { 
// do something 
}]; 

self.scrollView.delegate = nil; 
self.scrollView.delegate = self; 

注意,这里的delegate需要先设置为nil,再设置为self,而不能直接设置为self,如果self已经是该scrollView的Delegate的话。

有时,我们想对selector的返回值做一些处理,但很遗憾RAC不支持,如果真的有需要的话,可以使用Aspects

结语

这是搜集的一些RAC的常用方法,如果有不全的地方敬请谅解,但愿能带来一些帮助,有误的地方也欢迎指正和探讨。

分享一些关于RAC的文章,写得都很好

『状态』驱动的世界:ReactiveCocoa
『可变』的热信号 RACSubject
Pull-Driven 的数据流 RACSequence
优雅的 RACCommand
用于多播的 RACMulticastConnection
RAC 中的双向数据绑定 RACChannel
理解 RACScheduler 的实现
从代理到 RACSignal
【长篇高能】ReactiveCocoa 和 MVVM 入门
iOS MVVM+RAC 从框架到实战
ReactiveCocoa自述:工作原理和应用
美团网官方博客之RACSignal的Subscription深入分析

你可能感兴趣的:(ReactiveCocoa用法帮助)