ReactiveCocoa 学习 - 7

一些杂乱的总结

2016-06-15 | 阅读

RACSignal

信号,是我们最终要收到的信息.在ReactiveCocoa中,我们先预先构建信息流,即处理信息的方式,而不是等到事件发生(命令式).信号获取了这些异步方法(委托,回调block,通知,KVO,target/action事件观察等),并将它们统一到一个接口下,抽象为RACSignal.

RACSignal的发送者,成为 receiver,发送事件流给它的subscriber,目前有三种类型的事件 : next,error,completed.一个signalerrorcompleted终止前,可以发送任意数量的next事件.ReactiveCocoa使用category为很多基本的UIKit控件添加了signal,这样就可以直接对控件添加订阅了,如textfield的rac_textSignal:

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

这里是订阅textfield的内容改变的next事件.可以对消息进行一定的处理,如过滤消息:

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

这就体现了响应式编程的本质,根据数据流来表达响应的功能.信号由最初的数据,然后经过处理,在交付给订阅者.在ReactiveCocoa中,对消息流的处理定义在RACStream.h文件中,可以查看有很多操作,如map对原始消息进行转换. 每一个消息流处理的返回值都是一个RACSignal对象,即使用链式语法来处理数据流.

在这里,我先举一个例子,说明这样做的好处:

- (RACSignal *)emailAddressValidSignal {
if (!_emailAddressValidSignal) {
    _emailAddressValidSignal = [[[RACObserve(self, emailAddress)
                                map:^id(NSString *value) {
                                    if (value.length == 0) {
                                        return @(0);
                                    }else if([value isEmailAddress]){
                                        return @(1);
                                    }else {
                                        return @(2);
                                    }
                                }] distinctUntilChanged] replay];
}
return _emailAddressValidSignal;
}

[self.model.passwordValidSignal subscribeNext:^(NSNumber *x) {
    GETWEAK(self);
    NSInteger valid = [x integerValue];
    self.splitLine2.backgroundColor = _lineColorList[valid];
}];

[[[[self.nickNameTextField.rac_textSignal map:^id(NSString *value) {
    if (value.length == 0) {
        return @(NO);
    }else if([value isValidNickname]){
        return @(NO);
    }else {
        return @(YES);
    }
}] throttle:1.5] distinctUntilChanged]
subscribeNext:^(NSNumber *x) {
    GETWEAK(self);
    [self.view layoutIfNeeded]; // Ensures that all pending layout operations have been completed
    [UIView animateWithDuration:0.3 animations:^{
        [self.errorViewForNickName remakeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.nickNameTextField.bottom);
            make.left.right.equalTo(self.registerButton);
            make.height.equalTo([x boolValue] ? self.errorNicknameHeight :0);
        }];
        [self.view layoutIfNeeded];
    }];
}];

RAC

RAC宏允许直接把信号的输出应用到对象的属性上,如下:

RAC(self.passwordTextField, backgroundColor) = [validPasswordSignal
    map:^id(NSNumber *passwordValid){
      return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
    }];

RAC宏有两个参数,一个是需要设置属性值的对象,一个是属性的名称.每次信号产生next事件时,传递过来的值都会应用到该属性上.

通过RAC和 RACObserve传递的值可以一直传递下去.

聚合信号

如登录时,需要用户名和密码框输入都有效时,才能让登录按键有效,如何聚合两个信号呢?

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

combineLatest:reduce:方法,将两个信号产生的最新值聚合在一起,产生一个新的信号,然后执行reduce block,将结果发生给订阅者.这个方法可以聚合任意数量的信号,而reduce block的参数与每个源信号有关.对信号的整合操作在RACSignal+Operations.h,可以研究一下.

这样,有一个聚合信号,就可以直接与button的属性绑定了:

RAC(self.signInButton,enabled) = signUpActiveSignal;

通过这些信号,来操作信号,处理信号,响应信号,使用信号流,这样代码里就没有表示输入框有效状态的私有属性了,也就是在响应式编程中,不需要用实例变量来追踪瞬时状态.

整合异步操作消息

这里举点击登录,然后访问后台进行登录的一个简单流程.管理登录按键消息,通过信号rac_signalForControlEvents:

[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   subscribeNext:^(id x) {
     NSLog(@"button clicked");
   }];

异步访问有如下接口:

- (void)signInWithUsername:(NSString *)username
                  password:(NSString *)password
                  complete:(RWSignInResponse)completeBlock;

如何与ReactiveCocoa整合呢,我们直接创建一个信号:

- (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;
}];
}

使用createSignal:方法创建信号,方法的入参是一个block,这个block描述了这个信号,当这个信号有subscriber时,block里的代码就会执行.在block里可以发送一些next事件,也可以通过error/complelte事件来终止.block的返回值是一个RACDisposable对象,允许在一个订阅被取消时,进行一些清理工作,如这里可能需要中断网络连接,清理缓存之类的.

然后整合到登录按键中:

[[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   flattenMap:^id(id x){
     return [self signInSignal];
   }]
   subscribeNext:^(id x){
     NSLog(@"Sign in result: %@", x);
   }];

flattenMap是返回一个新的信号,而map是通过调用flattenMap实现的,通过新建一个返回map的block中内容的信号.

可以通过 doNext doError doCompleted方法来添加副作用,即在发送这些信号的同时,进行一些操作.

RACChannel 双向连接

使用宏 RACChannelTo(self, valueA)创建一个 指向valueA的KVO的 RACChannelTerminal ,即所有发向 valueA的信号都会被这个终端监听到.然后这个RACChannelTerminal可以将这个信号在进行转发. 看下例:

RACChannelTerminal *channelA = RACChannelTo(self, valueA);
RACChannelTerminal *channelB = RACChannelTo(self, valueB);
[[channelA map:^id(NSString *value) {
    if ([value isEqualToString:@"西"]) {
        return @"东";
    }
    return value;
}] subscribe:channelB];
[[channelB map:^id(NSString *value) {
    if ([value isEqualToString:@"左"]) {
        return @"右";
    }
    return value;
}] subscribe:channelA];
[[RACObserve(self, valueA) filter:^BOOL(id value) {
    return value ? YES : NO;
}] subscribeNext:^(NSString* x) {
    NSLog(@"你向%@", x);
}];
[[RACObserve(self, valueB) filter:^BOOL(id value) {
    return value ? YES : NO;
}] subscribeNext:^(NSString* x) {
    NSLog(@"他向%@", x);
}];
self.valueA = @"西";
self.valueB = @"左";

最终输出为 :

2016-05-05 00:07:10.211 MemoPlus[34730:1077055] 你向西
2016-05-05 00:07:10.212 MemoPlus[34730:1077055] 他向东
2016-05-05 00:07:10.212 MemoPlus[34730:1077055] 他向左
2016-05-05 00:07:10.213 MemoPlus[34730:1077055] 你向右

这里channelA截获到发往A的通道的信息后, 通过map转换了信号, 然后又通过subscribe:channelB发给通道B,这里是发送消息,所以是发送channelB所绑定的属性,也就是valueB,但是这次是直接赋值是走消息通道,而不会产生新的消息. 走信息通道,是不会继续传递下去的,而走普通的信号处理流程,即在subscribeNext中,就会陷入循环,如下情况:

 RAC(self,A) = RACObserve(self, B);
RAC(self,B) = RACObserve(self, A);
self.A = @"A";

这种情况就会陷入无限循环中…

信号流处理

  • flattenMap : 替换为新的信号
  • map : 替换信号值
  • filter : 过滤信号
  • ignore : 忽略信号
  • reduceEach : 多个信号的合并

    这个合并,指使用了zip或者combine后合并多个信号,而合并的时写法如下:

          // 这个block,按照自己定义信号的顺序和类型,进行填写,RAC会正确的传递参数进来.
         reduceEach:^id(NSNumber *signal1,NSString *signal2){
             return signal1;
         }]
    
  • skip 跳过一定数量的信号
  • take : 拿到指定数量的信号 与skip相反
  • zip : 压缩信号, 当多个信号都有最新值到达时,会组装成一个新的信号并发送. 必须所有组装的信号都有一个新的next发送.

信号操作

  • doNext , doError , doCompleted : 在一个信号发送的同事做一些副作用的操作
  • throttle : 节流,在一个时间间隔内,如果有新的信号到来,那使用新的信号,而抛弃旧的信号?
  • throttle :valuesPassingTest: : 节流,并进行通过检测,对于节流时,如果predicate返回YES,则被节流,如果返回NO,则直接发送消息.
  • delay : 延迟发送消息
  • repeat : 在信号完成后,重复
  • initially : 在信号发送前进行操作,指所有信号发送时都会做
  • finally : 在信号完成后进行操作,这个是指 complete和error信号.
  • bufferWithTime:onScheduler :
  • combine : 那最新的信号发送,当其中之一发送信号时,都会发送这条消息.同merge
  • merge : 是任何一个信号发送后,都会组装一次,发送一次信号
  • zip : zip才是不同的,必须两者都发送新的信号.
  • takeUntil : 直到一个信号发送,一直存在
  • then : 在一个信号完成时, 执行下一个信号 ,前面的信号必须是 complete,后面信号随意.

  • catch : 抓获异常 NSError.
  • rac_liftSelector:withSignals: 在signal初始化的sendnext后,每次新的siganl都会直接将结果植入selector的参数中,并调用函数.

defer : 将一个热信号转为冷信号,即等到被subscribe时,才发送消息.但是只是发送方式变成了冷,但是热信号的其他特征还在.

throttle节流

这个节流不是我想象中的节流,我的想象中一个合理的节流是,当产生到一个消息后,1个时间内段,不再接收新的消息,如一个地方的点击事件,一秒内只能点击一次,所以当产生一次点击消息后,抛弃一秒内其他所有点击消息.

而RAC所提供的throttle节流阀,是说,产生一个消息后,在一个时间段内部处理信号,而是等待新的信号,如果没有新的信号,则发送旧的信号,如果有新的信号,则替换这个新的信号,再继续等待这个节流时间间隔直至没有新的信号到达.

即随便写一个信号 :

[singalA throttle:10]

节流设置10秒后,那从发送信号到接受到信号,要等待10秒.如果又有新的信号,又要重新等待一次.

从我的角度来看,这种需要hold住信号,并等待一个时间间隔的节流,是没有这种必要的,不知道RAC是如何设想这种功能的使用场景的,反正我是看不懂.

RACCommand

特点是内部包含一个要执行的信号,然后能监控这个信号,可以知道真正要执行的信号什么时候开始,什么时候结束,以及爆出的异常.

rac_signalForSelector

rac_signalForSelector的方法,如果对象实现了目标方法,则会在执行目标方法后,执行signal的发送.而如果对象没有实现目标方法,则这个方法找不到selector,首先不会报错,然后如果是不需要返回值的回调,就可以假装是由订阅者实现了该回调方法.

但是目标方法的参数类型必须是 对象类型,且不能有返回值. 如果如不是,那依旧会因为没有实现该方法而出错.

用例子详细说明一下:

- (void) test:(NSString *)str {
    NSLog(@"test : %@",str);
}

 [[self rac_signalForSelector:@selector(test:)]
 subscribeNext:^(id x) {
     NSLog(@"subscribeNext %@",x);
 }];

[self test:@"hello world"];

输出为 :

2016-05-08 21:13:35.422 MemoPlus[8433:274799] test : hello world
2016-05-08 21:13:35.423 MemoPlus[8433:274799] subscribeNext <RACTuple: 0x7fd31bf3a720> (
    "hello world"
)

而如果当类中没有实现方法test时,直接执行test方法:

[self performSelector:@selector(test:) withObject:@"hello world"];

不会出错,能够正常运行.表现起来就像是 subscriber实现了该方法一样. 但是如果方法只能是全对象类型的,否则会报错. 然后返回一个空值给调用者.

还有一个相似的方法rac_signalForSelector:formProtocol,这个方法会从protocol中获取参数类型,所以就可以传递非对象类型的参数了.但是返回值依旧是空值.

RACObserve

使用KVO时,一般都会抓到第一次nil的赋值.所以一般都会设置skip1.

在RAC中,任何的信号转换操作都是对原有的信号进行订阅从而产生新的信号

对于RACDisposable的理解

RACDisposable是对信号的一次订阅的资源清理和取消订阅操作.举例如下:

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    NSLog(@"subscribe signal");
    [[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
        [subscriber sendNext:@"2S"];
    }];
    [[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{
        [subscriber sendNext:@"4S"];
    }];
    return [RACDisposable disposableWithBlock:^{
        NSLog(@"disposable with Nothing to do");
    }];
}];

[signal subscribeNext:^(id x) {
    NSLog(@"Subscription A send %@",x);
}];

RACDisposable *disposable = [signal subscribeNext:^(id x) {
    NSLog(@"Subscription B send %@",x);
}];

[[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{
    NSLog(@"At 3S ,dispose the Signal");
    [disposable dispose];
}];

最终输出为 :

2016-07-02 15:49:24.516 TestPods[19287:3277223] subscribe signal
2016-07-02 15:49:24.517 TestPods[19287:3277223] subscribe signal
2016-07-02 15:49:26.711 TestPods[19287:3277223] Subscription A send 2S
2016-07-02 15:49:26.712 TestPods[19287:3277223] Subscription B send 2S
2016-07-02 15:49:27.805 TestPods[19287:3277223] At 3S ,dispose the Signal
2016-07-02 15:49:27.805 TestPods[19287:3277223] disposable with Nothing to do
2016-07-02 15:49:28.916 TestPods[19287:3277223] Subscription A send 4S
2016-07-02 15:49:28.916 TestPods[19287:3277223] disposable with Nothing to do

在调用RACDisposabledispose方法后,信号就会被取消订阅.而信号执行完成时,即使没有completeerror时,也会调用最后的资源清理操作.