iOS响应式编程框架ReactiveCocoa讲解与实例演示

Signal and Subscriber

这是RAC最核心的内容,如果用插头和插座来描述,插座是Signal,插头是Subscriber。插座负责去获取电,插头负责使用电,而且一个插座可以插任意数量的插头。当一个插座(Signal)没有插头 (Subscriber)时什么也不干,也就是处于冷(Cold)的状态,只有插了插头时才会去获取,这个时候就处于热(Hot)的状态。

Signal获取到数据后,会调用Subscriber的sendNext, sendComplete, sendError方法来传送数据给Subscriber,Subscriber自然也有方法来获取传过来的数据,如:[signal subscribeNext:error:completed]。这样只要没有sendComplete和sendError,新的值就会通过 sendNext源源不断地传送过来。

    RACObserve使用了KVO来监听property的变化,只要username被自己或外部改变,block就会被执行。但不是所有的property都可以被RACObserve,该property必须支持KVO,比如NSURLCache的currentDiskUsage就不能被RACObserve

Signal是很灵活的,它可以被修改(map),过滤(filter),叠加(combine),串联(chain),这有助于应对更加复杂的情况,比如:

RAC(self.logInButton, enabled) = [RACSignal
        combineLatest:@[
            self.usernameTextField.rac_textSignal,
            self.passwordTextField.rac_textSignal,
            RACObserve(LoginManager.sharedManager, loggingIn),
            RACObserve(self, loggedIn)
        ] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) {
            return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);
        }];

左边的RAC(...),它的作用是将self.logInButton.enabled属性与右边的signal的sendNext值绑定。也就是如果右边的reduce的返回值为NO,那么enabled就为NO。右边的combineLatest是获取这4个signal的next值。其中可以看到self.usernameTextField.rac_textSignal这么个东东,rac_textSignal是 RAC为UITextField添加的category,只要usernameTextField的值有变化,这个值就会被返回(sendNext)。 combineLatest需要每个signal至少都有过一次sendNext。reduce的作用是根据接收到的值,再返回一个新的值,这里是 @(YES)和@(NO),必须是object。

冷信号(Cold)和热信号(Hot)

RACSignal *signal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    NSLog(@"triggered");
    [subscriber sendNext:@"foobar"];
    [subscriber sendCompleted];
    return nil;
}];

创建了一个Signal,但因为没有被subscribe,所以什么也不会发生。所以此时处于冷信号状态

[signal subscribeCompleted:^{
    NSLog(@"subscription %u", subscriptions);
}];

加了上面这段代码后,signal就处于Hot的状态了,block里的代码就会被执行。

或许有人会问,如果这时又有一个新的subscriber了,signal的block还会被执行吗?这就牵扯到了另一个概念:Side Effect

Side Effect

还是上面那段代码,如果有多个subscriber,那么signal就会又一次被触发,控制台里会输出两次triggered。这或许是你想要的,或许不是。如果要避免这种情况的发生,可以使用[ signal replay ]方法,它的作用是保证signal只被触发一次,然后把sendNext的value存起来,下次再有新的subscriber时,直接发送缓存的数据。

Cocoa Categories

为了更加方便地使用RAC,RAC给Cocoa添加了很多category,与系统集成地越紧密,使用起来自然也就越方便。下面是我认为比较常用的categories。

  UIView Categories

上面看到的rac_textSignal是加在UITextField上的(UITextField+RACSignalSupport.h),其他常用的UIView也都有添加相应的category,比如UIAlertView,就不需要再用Delegate了。

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"" message:@"Alert" delegate:nil cancelButtonTitle:@"YES" otherButtonTitles:@"NO", nil];
[[alertView rac_buttonClickedSignal] subscribeNext:^(NSNumber *indexNumber) {
    if ([indexNumber intValue] == 1) {
        NSLog(@"you touched NO");
    } else {
        NSLog(@"you touched YES");
    }
}];
[alertView show];

有了这些Category,大部分的Delegate都可以使用RAC来做。或许你会想,可不可以subscribe NSMutableArray.rac_sequence.signal,这样每次有新的object或旧的object被移除时都能知 道,UITableViewController就可以根据dataSource的变化,来reloadData。但很可惜这样不行,因为RAC是基于 KVO的,而NSMutableArray并不会在调用addObject或removeObject时发送通知,所以不可行。不过可以使用 NSArray作为UITableView的dataSource,只要dataSource有变动就换成新的Array,这样就可以了。

说到UITableView,再说一下UITableViewCell,RAC给UITableViewCell提供了一个方法:rac_prepareForReuseSignal,它的作用是当Cell即将要被重用时,告诉Cell。想象Cell上有多个button,Cell在初始化时给每个button都addTarget:action:forControlEvents,被重用时需要先移除这些target,下面这段代码就可以很方便地解决这个问题:

[[[self.cancelButton rac_signalForControlEvents:UIControlEventTouchUpInside]
    takeUntil:self.rac_prepareForReuseSignal]
    subscribeNext:^(UIButton *x) {
    // do other things
}];

还有一个很常用的category就是UIButton+RACCommandSupport.h,它提供了一个property:rac_command,就是当button被按下时会执行的一个命令,命令被执行完后可以返回一个signal,有了signal就有了灵活性。比如点击投票按钮,先判断一下有没有登录,如果有就发HTTP请求,没有就弹出登陆框,可以这么实现。

 logInButton.rac_command = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {        return [RACSignal empty];    }];
//下面的代码摘自AshFurrow的FunctionalReactivePixels。voteButton.rac_command = [[RACCommand alloc] initWithEnabled:self.viewModel.voteCommand.enabled signalBlock:^RACSignal *(id input) {
    // Assume that we‘re logged in at first. We‘ll replace this signal later if not.
    RACSignal *authSignal = [RACSignal empty];

    if ([[PXRequest apiHelper] authMode] == PXAPIHelperModeNoAuth) {
        // Not logged in. Replace signal.
        authSignal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            @strongify(self);

            FRPLoginViewController *viewController = [[FRPLoginViewController alloc] initWithNibName:@"FRPLoginViewController" bundle:nil];
            UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];

            [self presentViewController:navigationController animated:YES completion:^{
                [subscriber sendCompleted];
            }];

            return nil;
        }]];
    }

    return [authSignal then:^RACSignal *{
        @strongify(self);
        return [[self.viewModel.voteCommand execute:nil] ignoreValues];
    }];
}];
[voteButton.rac_command.errors subscribeNext:^(id x) {
    [x subscribeNext:^(NSError *error) {
        [SVProgressHUD showErrorWithStatus:[error localizedDescription]];
    }];
}];

Data Structure Categories

常用的数据结构,如NSArray, NSDictionary也都有添加相应的category,比如NSArray添加了rac_sequence,可以将NSArray转换为RACSequence,顺便说一下RACSequence, RACSequence是一组immutable且有序的values,不过这些values是运行时计算的,所以对性能提升有一定的帮助。RACSequence提供了一些方法,如array转换为NSArrayany:检查是否有Value符合要求,all:检查是不是所有的value都符合要求,这里的符合要求的,block返回YES,不符合要求的就返回NO。

NotificationCenter Category

   NSNotificationCenter, 默认情况下NSNotificationCenter使用Target-Action方式来处理Notification,这样就需要另外定义一个方法,这就涉及到编程领域的两大难题之一:起名字。有了RAC,就有Signal,有了Signal就可以subscribe,于是NotificationCenter就可以这么来处理,还不用担心移除observer的问题。

[[[NSNotificationCenter defaultCenter] rac_addObserverForName:@"MyNotification" object:nil] subscribeNext:^(NSNotification *notification) {
    NSLog(@"Notification Received");
}];

NSObject Categories

NSObject有不少的Category,我觉得比较有用的有这么几个

NSObject+RACDeallocating.h

顾名思义就是在一个object的dealloc被触发时,执行的一段代码。

NSArray *array = @[@"foo"];
[[array rac_willDeallocSignal] subscribeCompleted:^{
    NSLog(@"oops, i will be gone");
}];
array = nil;

NSObject+RACLifting.h

有时我们希望满足一定条件时,自动触发某个方法,有了这个category就可以这么办

- (void)test
{
    RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        double delayInSeconds = 2.0;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
            [subscriber sendNext:@"A"];
        });
        return nil;
    }];

    RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"B"];
        [subscriber sendNext:@"Another B"];
        [subscriber sendCompleted];
        return nil;
    }];

    [self rac_liftSelector:@selector(doA:withB:) withSignals:signalA, signalB, nil];
}

- (void)doA:(NSString *)A withB:(NSString *)B
{
    NSLog(@"A:%@ and B:%@", A, B);
}

这里的rac_liftSelector:withSignals 就是干这件事的,它的意思是当signalA和signalB都至少sendNext过一次,接下来只要其中任意一个signal有了新的内容,doA:withB这个方法就会自动被触发。

如果你有兴趣,可以想想上面这段代码会输出什么。

NSObject+RACSelectorSignal.h

这个category有rac_signalForSelector:rac_signalForSelector:fromProtocol: 这两个方法。先来看前一个,它的意思是当某个selector被调用时,再执行一段指定的代码,相当于hook。比如点击某个按钮后,记个日志。后者表示该selector实现了某个协议,所以可以用它来实现Delegate。

MVVM

RAC带来的变化还不仅仅是这些,它还带来了架构层面的变化。我们都知道苹果推荐的是MVC架构,那MVVM又是什么呢?

跟MVC最大的区别是多了个ViewModel,它直接与View绑定,而且对View一无所知。拿做菜打比方的话,ViewModel就是调料,它不关心做的到底是什么菜。这不是跟Model很像吗?是的,它可以扮演Model的职责,但其实它是Model的中介,这样当Model的API有变化,或者由本地存储变为远程API调用时,ViewModel的public API可以保持不变。

使用ViewModel的好处是,可以让Controller更加简单和轻便,而且ViewModel相对独立,也更加方便测试和重用。那 Controller这时又该做哪些事呢?在MVVM体系中,Controller可以被看成View,所以它的主要工作是处理布局、动画、接收系统事 件、展示UI。

MVVM还有一个很重要的概念是 data binding,view的呈现需要data,这个data就是由ViewModel提供的,将view的data与ViewModel的data绑定后,将来双方的数据只要一方有变化,另一方就能收到。这里有Github 开源的一个ViewModel Base Class。

其他

RAC在使用时有一些注意事项,可以参考官方的DesignGuildLines,这里简单说一下。

当一个signal被一个subscriber subscribe后,这个subscriber何时会被移除?答案是当subscriber被sendComplete或sendError时,或者手动调用[disposable dispose]。

当subscriber被dispose后,所有该subscriber相关的工作都会被停止或取消,如http请求,资源也会被释放。

Signal events是线性的,不会出现并发的情况,除非显示地指定Scheduler。所以-subscribeNext:error:completed:里的block不需要锁定或者synchronized等操作,其他的events会依次排队,直到block处理完成。

Errors有优先权,如果有多个signals被同时监听,只要其中一个signal sendError,那么error就会立刻被传送给subscriber,并导致signals终止执行。相当于Exception。

生成Signal时,最好指定Name, -setNameWithFormat: 方便调试。

block代码中不要阻塞。

更多内容

实例

例1. 监听对象的成员变量变化,当成员变量值被改变时,触发做一些事情。

    场景1:当前类有一个属性 NSString *input,当它的值被改变时,发送一个请求。

[RACObserve(self, input)
    subscribeNext:^(id x){
        request(x);//发送一个请求
   }];

场景2:当前类有一个属性textField,当它的Text值被改变时,发送一个请求。

[RACObserve(self, textField.text) subscribeNext:^(id x) {
        request(x);//发送一个请求
    }];

场景1和2 注意:RACObserve(self, textField.text)中的第二参数一定要和当前类的属性相关,不能是全局变量或者局部变量。subscribeNext:^(id x)中就是RACObserve(self, textField.text)中的第二参数即textField.text。

场景3:在上面场景中,当用户输入的值以2开头时,才发请求.

[[RACObserve(self, input)
     filter:^(NSString* value){
         if ([value hasPrefix:@"2"]) {
             return YES;
         } else {
             return NO;
         }
     }]
     subscribeNext:^(NSString* x){
        request(x);//发送一个请求
    }];

         场景4:面场景是监听自己的成员变量,如果想监听UITextField输入值变化,框架也做了封装可以代替系统回调

[[self.priceInput.rac_textSignal
     filter:^BOOL(NSString *str) {
         if (str.integerValue > 20) {
             return YES;
         } else {
             return NO;
         }
     }]
     subscribeNext:^(NSString *str) {
              request(x);//发送一个请求
}]; 

例2. 同时监听多个变量变化,当这些变量满足一定条件时,使button为可点击状态

           场景1:button监听 两个输入框有值和一个属性Bool变量值,当输入框有输入且Bool为真时,button为可点击状态

RAC(self.payButton,enabled) = [RACSignal
                                   combineLatest:@[self.priceInput.rac_textSignal,
                                                self.nameInput.rac_textSignal,
                                                RACObserve(self, isConnected)
                                                ]
                                   reduce:^(NSString *price, NSString *name, NSNumber *connect){
                                   return @(price.length > 0 && name.length > 0 && [connect boolValue]);
                                   }];

场景1注意: combineLatest: reduce:中的关系层次和返回值.

场景2:满足上面条件时,直接发送请求

[[RACSignal
                                   combineLatest:@[self.priceInput.rac_textSignal,
                                                self.nameInput.rac_textSignal,
                                                RACObserve(self, isConnected)
                                                ]
                                   reduce:^(NSString *price, NSString *name, NSNumber *connect){
                                   return @(price.length > 0 && name.length > 0 && ![connect boolValue]);
                                   }]
                             subscribeNext:^(NSNumber *res){
                                 if ([res boolValue]) {
                                     NSLog(@"XXXXX send request");
                                 }
                             }];

场景4:用户每次在TextField中输入一个字符,1秒内没有其它输入时,去发一个请求。TextField中字符改变触发事件已在例1中展示,这里实现一下它触法的方法,把1秒延时在此方法中实现。

- (void)showLoading {

    [self.loadingDispose dispose];//上次信号还没处理,取消它(距离上次生成还不到1秒)
    @weakify(self);
    self.loadingDispose = [[[RACSignal createSignal:^RACDisposable *(id<racsubscriber> subscriber) {
        [subscriber sendCompleted];
        return nil;
    }] delay:1] //延时一秒
    subscribeCompleted:^{
        @strongify(self);
        doRequest();
        self.loadingDispose = nil;
    }];
}
时间: 2024-10-26 05:24:44

iOS响应式编程框架ReactiveCocoa讲解与实例演示的相关文章

IOS响应式编程框架ReactiveCocoa(RAC)使用示例

ReactiveCocoa是响应式编程(FRP)在IOS中的一个实现框架,它的开源地址为:https://github.com/ReactiveCocoa/ReactiveCocoa# :在网上看了几篇文章,感觉理论讲了很多,但是代码还是看不太懂,于是自己把它github文档上的一些使用的经典示例实现了一下,项目中有需要时可以直接搬过去用,用的熟练了再读源码也比较容易理解. 例1. 监听对象的成员变量变化,当成员变量值被改变时,触发做一些事情. 这种情况其实就是IOS KVO机制使用的场景,使用

IOS响应式编程框架ReactiveCocoa(RAC)使用示例-备

ReactiveCocoa是响应式编程(FRP)在IOS中的一个实现框架,它的开源地址为:https://github.com/ReactiveCocoa/ReactiveCocoa# :在网上看了几篇文章,感觉理论讲了很多,但是代码还是看不太懂,于是自己把它github文档上的一些使用的经典示例实现了一下,项目中有需要时可以直接搬过去用,用的熟练了再读源码也比较容易理解. 例1. 监听对象的成员变量变化,当成员变量值被改变时,触发做一些事情. 这种情况其实就是IOS KVO机制使用的场景,使用

响应式编程框架ReactiveCocoa学习——框架概览

这篇博客将会继续翻译RAC的官方文档Framework Overview. 主要是对RAC这和框架进行概览的介绍和学习.同时也可以参考我前面的两篇翻译<响应式编程框架ReactiveCocoa学习--基本操作符><响应式编程框架ReactiveCocoa介绍与入门>.其中ReactiveCocoa的Github官方地址为 https://github.com/ReactiveCocoa/ReactiveCocoa . 这篇文档包括了RAC中的对不同组件的高层描述,并解释如何进行结合

高大上函数响应式编程框架ReactiveCocoa学习笔记1 简介

原创文章,转载请声明出处哈. ReactiveCocoa函数响应式编程 一.简介 ReactiveCocoa(其简称为RAC)是函数响应式编程框架.RAC具有函数式编程和响应式编程的特性.它主要吸取了.Net的 Reactive Extensions的设计和实现. 函数式编程 (Functional Programming) 函数式编程也可以写N篇,它是完全不同于OO的编程模式,这里主要讲一下这个框架使用到的函数式思想. 1) 高阶函数:在函数式编程中,把函数当参数来回传递,而这个,说成术语,我

[iOS] 响应式编程开发-ReactiveCocoa(二)

RAC实现图片下载功能 在实现异步RAC下载图片的过程中,需要注意以下几点: • 通过 NSURLConnection 对象的 +(RACSignal *)rac_sendAsynchronousRequest:(NSURLRequest*)r 可以创建网络异步下载的信号量对象: • 在执行完成异步下载过程后,需要通过 [x deliverOn:[RACScheduler mainThreadScheduler]] 操作将方法返回到主线程进行执行: @implementation ViewCon

ReactiveCocoa,最受欢迎的iOS函数响应式编程库(2.5版),没有之一!

ReactiveCocoa,最受欢迎的iOS函数响应式编程库(2.5版),没有之一! 简介 项目主页: ReactiveCocoa 实例下载: https://github.com/ios122/ios122 简评: 最受欢迎,最有价值的iOS响应式编程库,没有之一!iOS MVVM模式的完美搭档,更多关于MVVM与ReactiveCocoa的讨论,参考这篇文章: [长篇高能]ReactiveCocoa 和 MVVM 入门 注意: ReactiveCocoa 最新3.0版本,使用Swift重写,

[转帖]浅谈响应式编程(Reactive Programming)

浅谈响应式编程(Reactive Programming) https://www.jianshu.com/p/1765f658200a 例子写的非常好呢. 0.9312018.02.14 21:22:16字数 1877阅读 9816 这是告别CSDN后第一次使用简书写IT类的博客,还在适应.最不适应的就是不能直接手输markdown语法标记.(好像原因是我没有切换编辑器) 什么是响应式编程(Reactive Programming) In computing, reactive program

(10)响应式宣言、响应式系统与响应式编程——响应式Spring的道法术器

本系列文章索引<响应式Spring的道法术器>前情提要 响应式编程 | 响应式流 1.5 响应式系统 1.5.1 响应式宣言 关注"响应式"的朋友不难搜索到关于"响应式宣言"的介绍,先上图: 这张图凝聚了许多大神的智慧和经验,见官网,中文版官网,如果你认可这个宣言的内容,还可以签下你的大名.虽然这些内容多概念而少实战,让人感觉是看教科书,但是字字千金,不时看一看都会有新的体会和收获. 这也是新时代男朋友的行为准则: Responsive,要及时响应,24

Java的HTTP服务端响应式编程

为什么要响应式编程? 传统的Servlet模型走到了尽头 传统的Java服务器编程遵循的是J2EE的Servlet规范,是一种基于线程的模型:每一次http请求都由一个线程来处理. 线程模型的缺陷在于,每一条线程都要自行处理套接字的读写操作.对于大部分请求来讲,本地处理请求的速度很快,请求的读取和返回是最耗时间的.也就是说大量的线程浪费在了远程连接上,而没有发挥出计算能力.但是需要注意一点,线程的创建是有开销的,每一条线程都需要独立的内存资源.JVM里的-Xss参数就是用来调整线程堆栈大小的.而