函数响应式编程(FRP)框架--ReactiveCocoa

来源:互联网 发布:材料入库单软件 编辑:程序博客网 时间:2024/04/30 09:57

   因为工作原因,有段时间没更新博客了,甚是抱歉,不过,从今天开始我又活跃起来了,哈哈,于是决定每周更新一博,大家互相学习,交流。

  今天呢,讨论一下关于ReactiveCocoa,这个采用函数响应式编程(FRP)的框架,以下会对ReactiveCocoa简称为RAC。

  之前看过一遍文章,说的是作为一个iOS开发者,写的每一行代码几乎都是在相应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。我很赞同这种说法,于是,即然这样的话,那我们为什么不把这些需要响应的事件流统一放在一起呢。很幸运的是,Github有一个开源项目,即 ReactiveCocoa简称RAC,就是基于响应式编程思想的Objective-C实践。

  何为RAC呢:RAC是为应用中发生的不同事件流提供了一个标准接口,我们可以使用一些基本工具来更容易的连接、过滤和组合。

  RAC结合了几种编程风格:

  函数式编程(Functional Programming):使用高阶函数,例如函数用其他函数作为参数。

  响应式编程(Reactive Programming):关注于数据流和变化传播。

  所以,你可能听说过ReactiveCocoa被描述为函数响应式编程(FRP)框架。 


  有句比喻很好很形象的对RAC做了总结:

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


  首先导入RAC框架:

    可以通过CocoaPods导入RAC框架:

     platform:ios,'9.0'

    use_frameworks! #导入才不会报错

    pod'ReactiveCocoa'

  RAC为应用中发生的不同事件流提供了一个标准接口。在ReactiveCocoa术语中这个叫做信号(signal),由RACSignal类表示。

  ReactiveCocoa signal(RACSignal)发送事件流给它的subscriber(订阅者)。目前总共有三种类型的事件:next、error、completed。一个signal在因error终止或者完成前,可以发送任意数量的next事件。RACSignal有很多方法可以来订阅不同的事件类型。每个方法都需要至少一个block,当事件发生时就会执行block中的逻辑。

  ReactiveCocoa框架使用category来为很多基本UIKit控件添加signal。这样就能给控件添加订阅了,text field的rac_textSignal就是这么来的。

  看个例子:

       RACSignal *usernameSourceSignal =self.usernameTextField.rac_textSignal;

      RACSignal *filteredUsername = [usernameSourceSignalfilter:^BOOL(id value) {

       NSString *string = value;

       return string.length >3;   }];

        [filteredUsernamesubscribeNext:^(id x) {

       NSLog(@"%@",x);  }];

   在这里,rac_textSignal是起始事件。然后数据通过一个filter,如果这个事件包含一个长度超过3的字符串,那么该事件就可以通过。管道的最后一步就  是subscribeNext:block在这里打印出事件的值。 filter操作的输出也是RACSignal。

   其实,RACSignal的每个操作都会返回一个RACsignal,这在术语上叫做连贯接口(fluent interface)。这个功能可以让你直接构建管道,而不用每一步都使用本地变量。

  于是将上面的例子,可以变成:

      [[self.usernameTextField.rac_textSignalfilter:^BOOL(NSString* string) {

       return  string.length >3 ;

       }]subscribeNext:^(id x) { 

          NSLog(@"*** %@",x);   

      }];


类型转换:

    我们可以使用map(转换)操作通过block改变事件的数据。map从上一个next事件接收数据,通过执行block把返回值传给下一个next事件。在上面的代码中,mapNSString为输入,取字符串的长度,返回一个NSNumber。举个栗子:

  [[[self.usernameTextField.rac_textSignalmap:^id(NSString *text) {

        return@(text.length);

     }]filter:^BOOL(NSNumber *length ) {

        return  [lengthintegerValue];

     }]subscribeNext:^(id x) {

        NSLog(@"map___%@",x);

     }];


 能看到map操作之后的步骤收到的都是NSNumber实例。你可以使用map操作来把接收的数据转换成想要的类型,只要它是个对象。


创建有效状态信号:

     首先要做的就是创建一些信号,比如常见的登录界面,来表示用户名和密码输入框中的输入内容是否有效,如:

      RACSignal *validUsernameSignal = [self.usernameTextField.rac_textSignalmap:^id(NSString *text) {

       return@([selfisValidUsername:text]);

    }];

   RACSignal *validPasswordSignal = [self.passwordTextField.rac_textSignalmap:^id(id value) {

       NSString *text = value;

       return@([selfisValidPassword:text]);   

    }];


   我们可以转换这些信号,从而能为输入框设置不同的背景颜色。基本上就是,你订阅这些信号,然后用接收到的值来更新输入框的背景颜色。

  使用RAC 

     RAC宏允许直接把信号的输出应用到对象的属性上。RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。每次信号产生一个next事件,传递过来的值都会应用到该属性上。

     

 RAC(self.passwordTextField,backgroundColor) = [validPasswordSignalmap:^id(NSNumber *passwordValid) {

       NSLog(@"%d",[passwordValidboolValue]);

       return [passwordValidboolValue]?[UIColorclearColor]:[UIColoryellowColor];

    }];

   RAC(self.usernameTextField,backgroundColor) = [validUsernameSignalmap:^id(NSNumber *usernameValid) {

       NSLog(@"%d",[usernameValidboolValue]);

       return [usernameValidboolValue]?[UIColorclearColor]:[UIColoryellowColor];

    }];


信号聚合

  使用combineLatest:reduce:方法把几个信号RACSignal产生的最新的值聚合在一起,并生成一个新的信号。每次这两个源信号的任何一个产生新值时,reduce block都会执行,block的返回值会发给下一个信号。

    RACSignal *signUpActiveSignal = [RACSignalcombineLatest:@[validUsernameSignal,validPasswordSignal]reduce:^id(NSNumber *usernameValid,NSNumber *passwordValid){

       return@([usernameValidboolValue]&&[passwordValidboolValue]);

    }];

    [signUpActiveSignalsubscribeNext:^(NSNumber *signupActive) {

       self.signInButton.enabled = [signupActiveboolValue];

    }];

ReactiveCocoa处理按钮的事件 

    需要用到ReactiveCocoaUIKit添加的另一个方法,rac_signalForControlEvents。

信号中的信号

   使用map方法,把按钮点击信号转换成了登录信号。subscriber输出log,把map操作改成flattenMap,这个操作把按钮点击事件转换为登录信号,同时还从内部信号发送事件到外部信号。


  doNext:是直接跟在按钮点击事件的后面。而且doNext: block并没有返回值。因为它是附加操作,并不改变事件本身。  

    [[[[self.signInButton

       rac_signalForControlEvents:UIControlEventTouchUpInside]

      doNext:^(id x){

          self.signInButton.enabled =NO;

          self.signInFailureText.hidden =YES;

       }]

     flattenMap:^id(id x){

         return[selfsignInSignal];

      }]

    subscribeNext:^(NSNumber*signedIn){

        self.signInButton.enabled =YES;

        BOOL success =[signedInboolValue];

        self.signInFailureText.hidden = success;

        if(success){

             [selfperformSegueWithIdentifier:@"signInSuccess"sender:self];

         }

     }];


创建信号

     使用RACSignalcreateSignal:方法来创建信号。方法的参数是一个block,这个block描述了这个信号。当这个信号有subscriber时,block里的代码就会执行。

 block的参数是一个subscriber实例,它遵循RACSubscriber协议,协议里有一些方法来产生事件,你可以发送任意数量的next事件,或者用error\complete事件来终止。本例中,信号发送了一个next事件来表示登录是否成功,随后是一个complete事件。 

 这个block的返回值是一个RACDisposable对象,它允许你在一个订阅被取消时执行一些清理工作。当前的信号不需要执行清理操作,所以返回nil就可以了。

- (RACSignal *)signInSignal {

   return [RACSignalcreateSignal:^RACDisposable *(id subscriber){

        [self.signInService

        signInWithUsername:self.usernameTextField.text

        password:self.passwordTextField.text

        complete:^(BOOL success){

             [subscribersendNext:@(success)];

             [subscribersendCompleted];

         }];

       returnnil;

    }];

}


总结:

       ReactiveCocoa的核心就是信号,而它不过就是事件流,ReactiveCocoa的主旨是让你的代码更简洁易懂,这值得多想想。


这里将附上demo,欢迎大家阅读。









0 0