iOSiOS之框架架构iOS 响应式编程

ReactiveCocoa(RAC)-iOS

2016-08-31  本文已影响8344人  PHM

简介


ReactiveCocoa(简称为RAC),RAC具有函数响应式编程特性,由Matt Diephouse开源的一个应用于iOS和OS X的新框架。

为什么使用RAC?


因为RAC具有高聚合低耦合的思想所以使用RAC会让代码更简洁,逻辑更清晰。

如何在项目中添加RAC?


platform:ios, '7.0'
pod 'ReactiveCocoa','~>2.0'

工作原理


工作原理

常见类解释


1. Stream - 信号流值 - RACStream类
表示一个基本单元可以为任意值,其值会随着事件的变化而变化,可以在其上进行一些复杂的操作运算(map,filter,skip,take等.)此类不会被经常使用, 多情况下表现为signal和sequences(RACSignal 和RACSequence继承于RACStream类)

[[RACObserve(self, reactiveString)
    filter:^BOOL(NSString *value) {
        return [value hasPrefix:@"A"];
}]
subscribeNext:^(NSString *value) {
        NSLog(@"%@",value);
}];

2. Signals - 信号 - RACSignal类

RACSignal能力

什么是Signals?


Signals

有订阅者监听时信号才会发信息, Signals会向那个订阅者发送0或多个载有数值的”next”事件,后面跟着一个”complete”事件或一个”error”事件。
Signals会发送三种不同信号给Subscriber

订阅者监听
__block int aNumber = 0;
// Signal that will have the side effect of incrementing `aNumber` block
// variable for each subscription before sending it.
RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    aNumber++;
    [subscriber sendNext:@(aNumber)];
    [subscriber sendCompleted];
    return nil;
}];
        
// This will print "subscriber one: 1"
[aSignal subscribeNext:^(id x) {
    NSLog(@"subscriber one: %@", x);
}];
        
// This will print "subscriber two: 2"
[aSignal subscribeNext:^(id x) {
    NSLog(@"subscriber two: %@", x);
}];

如果需要对信号进行过滤,转换,分解和合并那些值的话则不同的订阅者可能需要使用信号通过不同方式发送的值。


信号处理
RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, usernameValid);
RAC(self.Button, alpha) = [usernameIsValidSignal
    map:^(NSNumber *valid) {
        return valid. boolValue?@1:@0.5;
}];

3. Subscriber - 订阅者 - RACSubscriber协议
表示能够接收信号的对象,订阅信号才会激活信号,实现RACSubscriber协议的对象都可以为订阅者。
可以通过- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock 订阅信号。

RACSignal *repeatSignal = [[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]] repeat];
[repeatSignal subscribeNext: ^(NSDate* time){
      NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
      [formatter setDateFormat:@"HH:mm:ss"];
      NSLog(@"%@",[formatter stringFromDate:time]);
}];

4. Subjects - 手动控制信号 - RACSubject
表示可以手动控制信号,
处理流程:创建信号-订阅信号-发送信号

// 1.创建信号
RACSubject *subject = [RACSubject subject];
// 2.订阅信号 First
[subject subscribeNext:^(id x) {
    // block调用时刻:当信号发出新值,就会调用.
      NSLog(@"FirstSubscribeNext%@",x);
}];
// 2.订阅信号 Second
[subject subscribeNext:^(id x) {
      // block调用时刻:当信号发出新值,就会调用.
      NSLog(@"SecondSubscribeNext%@",x);
}];
// 3.发送信号
[subject sendNext:@"1"];
[subject sendNext:@"2"];

也是RAC代码与非RAC代码的Bridge 所以非常有用,此类继承于RACSignal类。

5. ReplaySubject - 手动回放控制信号 - RACReplaySubject
表示可以手动控制信号,底层实现和RACSubject不一样,它会先把值保存起来,然后遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock然后调用subscribeNext订阅信号,遍历保存的所有值,一个一个调用订阅者的nextBlock。
可以有以下两种处理流程:

处理流程 1:创建信号-订阅信号-发送信号(和Subjects一样)
处理流程 2:创建信号-发送信号-订阅信号

// 1.创建信号
RACReplaySubject *replaySubject = [RACReplaySubject subject];
// 2.发送信号
[replaySubject sendNext:@"1"];
[replaySubject sendNext:@"2"];
// 3.订阅信号 First
[replaySubject subscribeNext:^(id x) {
      NSLog(@"FirstSubscribeNext%@",x);
}];
// 3.订阅信号 Second
[replaySubject subscribeNext:^(id x) {
      NSLog(@"SecondSubscribeNext%@",x);
}];

6. Command- 命令信号 - RACCommand
表示订阅响应Action信号,通常由UI来出发,比如一个Button当控件被触发时会被自动禁用掉。

UIButton *reactiveBtn = [[UIButton alloc] init];
[reactiveBtn setTitle:@"点我" forState:UIControlStateNormal];
    reactiveBtn.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(UIButton *input) {
    NSLog(@"点击了我:%@",input.currentTitle);
    //返回一个空的信号量
    return [RACSignal empty];
}];

7. Sequences- 集合 - RACSequence
表示一个不可变的序列值且不能包含空值,使用-rac_sequence.signal来获取Signal。

RACSignal *signal = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
// Outputs
[signal subscribeNext:^(NSString *x) {
    NSLog(@"%@", x);
}];

8. Disposables- 清理订阅 - RACDisposable
表示用于取消信号的订阅,当一个signal被subscriber后,当执行sendComplete或sendError时subscriber会被移除,或者手动调用[disposable dispose]进行移除操作。
当subscriber被移除后,所有该subscriber相关的工作都会被停止或取消,如http请求,资源也会被释放。

9. Scheduler- 计划 - RACScheduler
表示一个信号队列,是信号执行任务时所在的队列或者信号执行完成后将结果放到队列里执行,它支持取消对列里的执行并总是串行执行。

RAC常用宏


RACObserve(TARGET, KEYPATH)
表现形式:RACObserve(self, stringProperty)
KVO的简化版本 相当于对TARGET中KEYPATH的值设置监听,返回一个RACSignal

RAC(TARGET, ...)
表现形式:RAC(self, stringProperty) = TextField.rac_textSignal
第一个是需要设置属性值的对象,第二个是属性名
RAC宏允许直接把信号的输出应用到对象的属性上
每次信号产生一个next事件,传递过来的值都会应用到该属性上

RACChannelTo(TARGET, ...)
RACChannelTo 用于双向绑定
RACChannelTo(self, stringProperty)=RACChannelTo(self.label, text) ;

RAC结构图


RAC结构图

RAC基础使用


创建一个TextField名为usernameTextField 设置监听TextField

[self.usernameTextField.rac_textSignal 
    subscribeNext:^(id x){
    NSLog(@"%@", x);
}];

filter:如果想添加一个条件 只输出x的长度大于3的,可以使用filter操作来实现这个目的

[self.usernameTextField.rac_textSignal
    filter:^BOOL(NSString* text){
    return text.length > 3;
}];
filter

map:把text转换成length进行输出,使用map可以对信号进行转换,一个源信号转换成另外一个新的信号输出

[[[self.usernameTextField.rac_textSignal
map:^id(NSString*text){
  return @(text.length);
}]
filter:^BOOL(NSNumber*length){
  return[length integerValue] > 3;
}]
subscribeNext:^(id x){
  NSLog(@"%@", x);
}];
map

信号可聚合也可以分割

聚合: 多个信号可以聚合成一个新的信号,这个可以是任何类型的信号

RACSignal *signal =
[RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
                    reduce:^id(NSNumber*usernameValid, NSNumber *passwordValid){
                      return @([usernameValid boolValue]&&[passwordValid boolValue]);
                    }];

分割:一个信号可以有很多subscriber,也就是作为很多后续步骤的源

RACSignal *signal = self.usernameTextField.rac_textSignal;
    [signal subscribeNext:^(id x) {
        NSLog(@"1111");
    }];
    [signal subscribeNext:^(id x) {
        NSLog(@"2222");
    }];
}

RAC设置Button的ControlEvents

[[self.signInButton
     rac_signalForControlEvents:UIControlEventTouchUpInside]
     subscribeNext:^(id x) {
     NSLog(@"button click");
}];
rac_signalForControlEvents
登陆功能举例说明
需要实现登陆功能需要点击登陆button
- (RACSignal *)signInSignal {
    return [RACSignal createSignal:^RACDisposable *(id subscriber){
     [self.signInService 
     signInWithUsername:self.usernameTextField.text
               password:self.passwordTextField.text
               complete:^(BOOL success){
                    [subscriber sendNext:@(success)];
                    [subscriber sendCompleted];
       }];
     return nil;
    }];
}
[[[[self.signInButton
     rac_signalForControlEvents:UIControlEventTouchUpInside]
     doNext:^(id x){
       self.signInButton.enabled =NO;
       self.signInFailureText.hidden =YES;
     }]

    flattenMap:^id(id x){        
        return[self signInSignal];
    }]

    subscribeNext:^(NSNumber*signedIn){
    self.signInButton.enabled =YES;
    BOOL success =[signedIn boolValue];
    self.signInFailureText.hidden = success;
    if(success){
        [self performSegueWithIdentifier:@"signInSuccess" sender:self];
    }
}];

flattenMap:[self signInSignal]返回的也是signal,所以是信号中的信号,使用这个操作把按钮点击事件转换为登录信号,同时还从内部信号发送事件到外部信号。

doNext:为一个附加操作,在一个next事件发生时执行的逻辑,而该逻辑并不改变事件本身。

流程

RAC高级使用


error 和 completed,节流,线程,延伸,其他

内存管理

ReactiveCocoa设计的一个目标就是支持匿名生成管道这种编程风格。到目前为止,在你所写的所有响应式代码中,这应该是很直观的。
为了支持这种模型,ReactiveCocoa自己持有全局的所有信号。如果一个signal有一个或多个订阅者,那这个signal就是活跃的。如果所有的订阅者都被移除了,那这个信号就能被销毁了。

如何取消订阅一个signal?
在一个completed或者error事件之后,订阅会自动移除。你还可以通过RACDisposable 手动移除订阅。

RACSignal的订阅方法都会返回一个RACDisposable实例,它能让你通过dispose方法手动移除订阅。这个方法并不常用到,但是还是有必要知道可以这样做。

RACSignal *backgroundColorSignal =
  [self.searchText.rac_textSignal 
      map:^id(NSString *text) { 
          return [self isValidSearchText:text] ? 
              [UIColor whiteColor] : [UIColor yellowColor]; 
  }]; 

RACDisposable *subscription = 
[backgroundColorSignal 
    subscribeNext:^(UIColor *color) {
        self.searchText.backgroundColor = color; 
}]; 

[subscription dispose];​

避免循环引用
在ReactiveCocoa中提供了避免循环引用的方法
@weakify宏让你创建一个弱引用的影子对象(如果你需要多个弱引用,你可以传入多个变量),
@strongify让你创建一个对之前传入@weakify对象的强引用。

@weakify(self) 
[[self.searchText.rac_textSignal 
map:^id(NSString *text) { 
    return [self isValidSearchText:text] ? 
        [UIColor whiteColor] : [UIColor yellowColor]; 
}] 
subscribeNext:^(UIColor *color) { 
    @strongify(self) 
    self.searchText.backgroundColor = color; 
}];​

signal能发送3种不同类型的事件
Next
Completed
Error

当应用获取访问社交媒体账号的权限时,用户会看见一个弹框。这是一个异步操作,因此把这封装进一个signal是很好的选择

-(RACSignal *)requestAccessToTwitterSignal {
// 1 - define an error 
NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                           code:RWTwitterInstantErrorAccessDenied 
                                       userInfo:nil];
                                   
// 2 - create the signal 
@weakify(self) 
return [RACSignal createSignal:^RACDisposable *(id subscriber) { 
    // 3 - request access to twitter 
    @strongify(self) 
    [self.accountStore requestAccessToAccountsWithType:self.twitterAccountType 
           options:nil 
        completion:^(BOOL granted, NSError *error) {
        // 4 - handle the response 
        if (!granted) { 
           [subscriber sendError:accessError]; 
        } else { 
            [subscriber sendNext:nil]; 
            [subscriber sendCompleted]; 
        } 
    }]; 
return nil; 
}]; 
}​

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

[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
subscribeNext:^(id x) { 
    NSLog(@"%@", x); 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];​
then

实时搜索内容方法

-(SLRequest *)requestforTwitterSearchWithText:(NSString *)text { 
NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/1.1/search/tweets.json"]; 
NSDictionary *params = @{@"q" : text}; 
SLRequest *request = [SLRequest   requestForServiceType:SLServiceTypeTwitter 
                                    requestMethod:SLRequestMethodGET 
                                              URL:url 
                                       parameters:params]; 
return request; 
}​
-(RACSignal *)signalForSearchWithText:(NSString *)text { 
// 1 - define the errors 
NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                               code:RWTwitterInstantErrorNoTwitterAccounts 
                                           userInfo:nil]; 
NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                                    code:RWTwitterInstantErrorInvalidResponse 
                                                    userInfo:nil]; 
                                                    
// 2 - create the signal block 
@weakify(self) 
return [RACSignal createSignal:^RACDisposable *(id subscriber) { 
    @strongify(self); 
    
    // 3 - create the request 
    SLRequest *request = [self requestforTwitterSearchWithText:text]; 
    
    // 4 - supply a twitter account 
    NSArray *twitterAccounts = [self.accountStore accountsWithAccountType:self.twitterAccountType];       
    if (twitterAccounts.count == 0) { 
        [subscriber sendError:noAccountsError]; 
    } else { 
        [request setAccount:[twitterAccounts lastObject]]; 
        
    // 5 - perform the request 
    [request performRequestWithHandler: ^(NSData *responseData, 
            NSHTTPURLResponse *urlResponse, NSError *error) { 
        if (urlResponse.statusCode == 200) { 
        
            // 6 - on success, parse the response 
            NSDictionary *timelineData = [NSJSONSerialization JSONObjectWithData:responseData 
                                            options:NSJSONReadingAllowFragments 
                                              error:nil]; 
            [subscriber sendNext:timelineData]; 
            [subscriber sendCompleted]; 
        } else { 
            // 7 - send an error on failure 
            [subscriber sendError:invalidResponseError]; 
        } 
    }]; 
} 
return nil; 
}];
}
[[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
flattenMap:^RACStream *(NSString *text) { 
    @strongify(self) 
    return [self signalForSearchWithText:text]; 
}] 
subscribeNext:^(id x) { 
    NSLog(@"%@", x); 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];

线程

在subscribeNext:error:中的数据没有在主线程(Thread 1)中执行,更新UI只能在主线程中执行,所以更新UI需要转到主线程中执行。

要怎么更新UI呢?
通常的做法是使用操作队列但是ReactiveCocoa有更简单的解决办法,在flattenMap:之后添加一个deliverOn:操作就可以转到主线程上了。
:如果你看一下RACScheduler类,就能发现还有很多选项,比如不同的线程优先级,或者在管道中添加延迟。

[[[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
flattenMap:^RACStream *(NSString *text) { 
    @strongify(self) 
    return [self signalForSearchWithText:text]; 
}] 
deliverOn:[RACScheduler mainThreadScheduler]] 
subscribeNext:^(id x) { 
    NSLog(@"%@", x); 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];

异步加载图片

-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl { 
RACScheduler *scheduler = [RACScheduler 
    schedulerWithPriority:RACSchedulerPriorityBackground]; 
    
return [[RACSignal createSignal:^RACDisposable *(id subscriber) { 
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]]; 
    UIImage *image = [UIImage imageWithData:data]; 
    [subscriber sendNext:image]; 
    [subscriber sendCompleted]; 
    return nil; 
}] subscribeOn:scheduler]; 
}

首先获取一个后台scheduler,来让signal不在主线程执行。然后,创建一个signal来下载图片数据,当有订阅者时创建一个UIImage。最后是subscribeOn:来确保signal在指定的scheduler上执行。

 -(UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"TableViewCell"];
[[[[self signalForLoadingImage:tweet.profileImageUrl] 
takeUntil:cell.rac_prepareForReuseSignal] 
deliverOn:[RACScheduler mainThreadScheduler]] 
subscribeNext:^(UIImage *image) { 
    cell.twitterAvatarView.image = image; 
}];
return cell;
}

cell是重用的,可能有脏数据,所以上面的代码首先重置图片。然后创建signal来获取图片数据。你之前也遇到过deliverOn:这一步,它会把next事件发送到主线程,这样subscribeNext:block就能安全执行了。

cell.rac_prepareForReuseSignal:Cell复用时的清理。
takeUntil:当给定的signal完成前一直取值

节流

每次输入一个字,搜索都会马上执行。如果你输入很快(或者只是一直按着删除键),这可能会造成应用在一秒内执行好几次搜索,这很不理想。
更好的解决方法是,当搜索文本在短时间内,比如说500毫秒,不再变化时,再执行搜索。
在filter之后添加一个throttle步骤:

[[[[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
throttle:0.5] 
flattenMap:^RACStream *(NSString *text) { 
    @strongify(self) 
    return [self signalForSearchWithText:text]; 
}] 
deliverOn:[RACScheduler mainThreadScheduler]] 
subscribeNext:^(NSDictionary *jsonSearchResult) { 
    NSArray *statuses = jsonSearchResult[@"statuses"]; 
    NSArray *tweets = [statuses linq_select:^id(id tweet) { 
        return [RWTweet tweetWithStatus:tweet]; 
    }]; 
    [self.resultsViewController displayTweets:tweets]; 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];

throttle:只有当前一个next事件在指定的时间段内没有被接收到后,throttle操作才会发送next事件。

代替代理

如果想在其他地方监听到tableView的代理信息则需要设置如下方法

[[tableView rac_signalForSelector:@selector(tableView:didSelectRowAtIndexPath:) fromProtocol:@protocol(UITableViewDelegate) ] subscribeNext:^(RACTuple * x) {
    NSLog(@"点击了");
}];

rac_signalForSelector: fromProtocol: 要先绑定在设置代理

测试Demo


ReactiveCocoaTest

官方链接地址


ReactiveCocoa开源地址
官方开发使用文档-Swift
官方开发使用文档-OC

扩展阅读


ReactiveCocoa 基本用法
说说ReactiveCocoa 2
学习RAC小记-适合给新手看的RAC用法总结
ReactiveCocoa-tutorial-pt1
ReactiveCocoa-tutorial-pt2
MVVC-RAC

上一篇下一篇

猜你喜欢

热点阅读