iOS开发ReactiveCocoa(一):介绍


君不见黄河之水天上来,奔流到海不复回。
君不见高堂明镜悲白发,朝如青丝暮成雪。
人生得意须尽欢,莫使金樽空对月。
天生我材必有用,千金散尽还复来。
烹羊宰牛且为乐,会须一饮三百杯。
岑夫子,丹丘生,将进酒,杯莫停。
与君歌一曲,请君为我侧耳听。
钟鼓馔玉不足贵,但愿长醉不复醒。
古来圣贤皆寂寞,惟有饮者留其名。
陈王昔时宴平乐,斗酒十千恣欢谑。
主人何为言少钱,径须沽取对君酌。
五花马,千金裘,
呼儿将出换美酒,与尔同销万古愁。

——李白《将进酒·君不见黄河之水天上来》

ReactiveCocoa

  • 是函数响应编程(Functional Reactive Programming, FRP)框架
  • 实际的开发中是和MVVM结合进行开发、互相弥补

Github上整合的比较全面的(MVVM、RAC)学习资料

美团的几篇资料 (开发中可能遇到的坑)

注意点

  • RAC只处理对象, 而不处理像BOOL这样的原始值. 不过, RAC通常会帮我们自动做这些转换.
  • 使用Cocoapods导入ReactiveCocoa 报错 ld: library not found for -lPods

    • 解决方法 : Target’s General settings and go to Linked Frameworks and Libraries and just delete -libPods.a from the list.
  • 信号的释放

    • ReactiveCocoa维护了一个全局的信号集合。
    • 如果信号有一个或多个订阅者,它就是可用的。如果所有订阅者都被移除了,信号就被释放了。
  • 信号的取消订阅

    • 在一个completed事件或error事件后,一个订阅者会自动将自己移除。
    • 手动移除可能通过RACDisposable来完成。RACSignal的所有订阅方法都返回一个RACDisposable实例,我们可以调用它的dispose方法来手动移除订阅者。
  • 如果我们创建了一个信号,但不去订阅它,则信号永远不会执行,包括任何如doNext:这样的附加操作。
  • Signal events是线性的,不会出现并发的情况,除非显式地指定Scheduler。所以subscribeNext:error:completed:里的block不需要锁定或者synchronized等操作,其他的events会依次排队,直到block处理完成。
  • 生成Signal时,最好指定Name, -setNameWithFormat: 方便调试
  • Side Effect (副作用)
    • Hot signals without side effects 最好使用property,如“textChanged”,不太理解什么情况用到这个,权当做一个静态的属性来看就行。
    • Cold signals without side effects 使用名词类型的方法名,如“-currentText”,“currentModels”,同时表明了返回值是什么(这个尤其得注意,RACSignal的next值是id类型,所以全得是靠约定才知道具体返回类型)
    • Signals with side effects 这种就是像login一样有副作用的了,推荐使用动词类型的方法名,用对动词基本就能知道是不是有副作用了,比如“-loginSignal”和“-saveToFile”大概就知道前面一个很可能有副作用,后面一个多存几次文件应该没副作用

RAC VS 原生

  • rac_signalForSelector:用于替代代理
  • rac_valuesAndChangesForKeyPath:用于监听某个对象的属性改变
  • rac_signalForControlEvents:用于监听某个事件。
  • rac_addObserverForName:用于监听某个通知
  • rac_textSignal:只要文本框发出改变就会发出这个信号
  • 处理当界面有多次请求时,需要都获取到数据时,才能展示界面
    • rac_liftSelector:withSignalsFromArray:Signals:当传入的Signals(信号数组),每一个signal都至少sendNext过一次,就会去触发第一个selector参数的方法
    • 使用注意:几个信号,参数一的方法就几个参数,每个参数对应信号发出的数据

RAC提供的宏

  • RAC()

    • 你提供想要与即将到来的值绑定的对象和参数, 在其内部它所做的是创建一个订阅者并更新其属性的值
    • 这个宏是最常用的,RAC()总是出现在等号左边,等号右边是一个RACSignal,表示的意义是将一个对象的一个属性和一个signal绑定,signal每产生一个value(id类型),都会自动执行:[TARGET setValue:value ?: NIL_VALUE forKeyPath:KEYPATH];

      1
      2
      3
      4
      5
      RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, isUsernameValid);
      RAC(self.goButton, enabled) = usernameIsValidSignal;
      RAC(self.goButton, alpha) = [usernameIsValidSignal map:^id(NSNumber *usernameIsValid){
      return usernameIsValid.boolValue ? @1.0 : @0.5;
      }];
  • RACObserve()

    • 这个宏是RAC中对KVO中那些API的替代. 你只需要传入对象和你想观察的那个对象某属性的 keypath. 给出这些参数后, RACObserve会创建一个信号, 一旦它有了订阅者, 它就立刻发送那个属性的当前值, 并在发送那个属性在这之后的任何变化.

      1
      2
      3
      4
      5
      6
      // create and get a reference to the signal 
      RACSignal *usernameValidSignal = RACObserve(self.viewModel, isUsernameValid);
      // update the local property when this value changes
      [usernameValidSignal subscribeNext: ^(NSNumber *isValidNumber) {
      self.usernameIsValid = isValidNumber. boolValue
      }];
  • 循环引用的宏

    • @weakify(self); // 在外面声明
    • @strongify(self); // 在引用的地方声明
    • 这两个宏一定成对出现,先weak再strong

常用知识点

  • map :
    • 将一个值转化为另一个值输出
    • 将会创建一个和原来一模一样的信号,只不过新的信号传递的值变为了block(value),也就是说,如果block(value)是一个信号,那么就是信号的value仍然是信号。
  • flattenMap : 解决Signal of Signals问题,flattenMap则会继续调用这个信号的value,作为新的信号的value。
  • switchToLatest : 用于signal of signals,获取signal of signals发出的最新信号,也就是可以直接拿到RACCommand中的信号
  • filter : 过滤一些条件,以确保符合条件的值才能被传递出去
  • ignore : 忽略给定的值

    • 注意,这里忽略的既可以是地址相同的对象,也可以是- isEqual:结果相同的值,也就是说自己写的Model对象可以通过重写- isEqual:方法来使- ignore:生效。常用的值的判断没有问题,如下:

      1
      2
      3
      4
      [[self.inputTextField.rac_textSignal ignore:@"seven"] subscribeNext:^(NSString *value)
      {
      NSLog(@"`seven` could never appear : %@", value);
      }];
  • distinctUntilChanged : 它将这一次的值与上一次做比较,当相同时(也包括- isEqual:)被忽略掉。

    1
    2
    3
    4
    RAC(self.label, text) = [RACObserve(self.user, username) distinctUntilChanged];
    self.user.username = @"sunnyxx"; // 1st
    self.user.username = @"sunnyxx"; // 2nd
    self.user.username = @"sunnyxx"; // 3rd
    • 如果不增加distinctUntilChanged的话对于连续的相同的输入值就会有不必要的处理,这个栗子只是简单的UI刷新,但遇到如写数据库,发网络请求的情况时,代价就不能购忽略了。
    • 所以,对于相同值可以忽略的情况,果断加上它吧。
  • 起止点过滤类型

    • 除了被动的当next值来的时候做判断,也可以主动的提前选择开始和结束条件,分为两种类型:
      • take型(取)
      • skip型(跳)
  • take:(NSUInteger) : 从开始一共取N次的next值,不包括CompetionError,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"1"];
    [subscriber sendNext:@"2"];
    [subscriber sendNext:@"3"];
    [subscriber sendCompleted];
    return nil;
    }] take:2] subscribeNext:^(id x) {
    NSLog(@"only 1 and 2 will be print: %@", x);
    }];
  • takeLast:(NSUInteger) : 取最后N次的next值

    • 注意,由于一开始不能知道这个Signal将有多少个next值,所以RAC实现它的方法是将所有next值都存起来,然后原Signal完成时再将后N个依次发送给接收者
    • 但Error发生时依然是立刻发送的
  • takeUntil:(RACSignal *) : 当给定的signal完成前一直取值, 也就是这个Signal一直到textField执行dealloc时才停止。最简单的栗子就是UITextField的rac_textSignal的实现(删减版本):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    - (RACSignal *)rac_textSignal {
    @weakify(self);
    return [[[[[RACSignal
    concat:[self rac_signalForControlEvents:UIControlEventEditingChanged]]
    map:^(UITextField *x) {
    return x.text;
    }]
    takeUntil:self.rac_willDeallocSignal] // bingo!
    }
  • takeUntilBlock:(BOOL(^)(id x)) : 对于每个next值,运行block,当block返回YES时停止取值,如:

    1
    2
    3
    4
    5
    [[self.inputTextField.rac_textSignal takeUntilBlock:^BOOL(NSString *value) {
    return [value isEqualToString:@"stop"];
    }] subscribeNext:^(NSString *value) {
    NSLog(@"current value is not `stop`: %@", value);
    }];
  • takeWhileBlock:(BOOL(^)(id x)) : 上面的反向逻辑,对于每个next值,block返回 YES时才取值

  • skip:(NSUInteger) : 从开始跳过N次的next值,简单的栗子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"1"];
    [subscriber sendNext:@"2"];
    [subscriber sendNext:@"3"];
    [subscriber sendCompleted];
    return nil;
    }] skip:1] subscribeNext:^(id x) {
    NSLog(@"only 2 and 3 will be print: %@", x);
    }];
  • skipUntilBlock:(BOOL(^)(id x)) : 和- takeUntilBlock:同理,一直跳,直到block为YES

  • skipWhileBlock:\(BOOL(^)(id x)) : 和- takeWhileBlock:同理,一直跳,直到block为NO
  • merge : 合并信号,只要有一个信号变化就会调用

    1
    2
    3
    4
    5
    6
    7
    8
    @weakify(self); 
    [[RACSignal merge: @[RACObserve(self.viewModel, tweets),
    RACObserve(self.viewModel, allTweetsLoaded)]]
    bufferWithTime: 0 onScheduler: [RACScheduler mainThreadScheduler]]
    subscribeNext: ^(id value) {
    @strongify(self);
    [self.tableView reloadData];
    }];
  • combineLatest : 将多个信号合并起来,并且拿到各个信号的最新的值,必须每个合并的signal至少都有过一次sendNext,才会触发合并的信号

    • 根据组合的信号的值再转换生成判断条件

      1
      2
      3
      4
      5
      6
      7
      RACSignal *signupActiveSignal = [RACSignal combineLatest:@[validUsernameSignal,
      validPwdSignal] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid) {
      return @(usernameValid.boolValue && passwordValid.boolValue);
      }];
      [signupActiveSignal subscribeNext:^(NSNumber *state) {
      self.signInButton.enabled = state.boolValue;
      }];
  • doNext: 注意doNext:并不返回一个值,因为它是附加操作。它完成时不改变事件。

  • concat: 按一定顺序拼接信号,当多个信号发出的时候,有顺序的接收信号 (只需要订阅拼接的信号就可以了)
  • then: then方法会等到completed事件发出后调用,然后订阅由then:block参数返回的信号。这有效地将控制从一个信号传递给下一个信号
  • zipWith: 把两个信号压缩成一个信号,只有当两个信号同时发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发压缩流的next事件

  • RACScheduler : 是RAC里面对线程的简单封装,事件可以在指定的scheduler上分发和执行,

    • 不特殊指定的话,事件的分发和执行都在一个默认的后台线程里面做,大多数情况也就不用动了
    • 有一些特殊的signal必须在主线程调用,使用-deliverOn:可以切换调用的线程。
  • deliverOn: 更新UI的时候回到主线程执行(子线程加载图片、主线程刷新UI)

    1
    deliverOn:[RACScheduler mainThreadScheduler]
  • chain :

  • multicast : 避免副作用,这两个订阅者接收到了同样的一个请求的内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Starts a single request, no matter how many subscriptions `connection.signal`
    // gets. This is equivalent to the -replay operator, or similar to
    // +startEagerlyWithScheduler:block:.
    RACMulticastConnection *connection = [networkRequest multicast:[RACReplaySubject subject]];
    [connection connect];
    [connection.signal subscribeNext:^(id response) {
    NSLog(@"subscriber one: %@", response);
    }];
    [connection.signal subscribeNext:^(id response) {
    NSLog(@"subscriber two: %@", response);
    }];
  • timeout : 超时,可以让一个信号在一定的时间后,自动报错

  • interval : 定时,每隔一段时间发出信号
  • delay : 延迟发送next
  • retry重试 : 只要失败,就会重新执行创建信号中的block,直到成功
  • replay重放 : 当一个信号被多次订阅,反复播放内容

  • throttle节流 : 当某个信号发送比较频繁时,可以使用节流,在某一段时间不发送信号内容,过了一段时间获取信号的最新内容发出

    • 延时处理,只有在两次next事件间隔指定的时间时才会发送第二个next事件
要不要鼓励一下😘