首页 ReactiveCocoa
文章
取消

ReactiveCocoa

很早就开始关注ReactiveCocoa了,前段时间决定把它加入到项目中,理由如下:

  • 熟悉响应式编程(函数式编程)模式的好时机
  • 整个框架经过0.0.1版本到2.3.1的迭代已经相对成熟
  • MVVM模式的尝试

从开始了解ReactiveCocoa到现在,有时候总感觉没有完全利用好,比如

1
2
3
4
5
@weakify(self);
[[self.nextButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
     @strongify(self);
     [self performSegueWithIdentifier:@"Captcha" sender:nil];
}];

这样单一的信号传递和部署,没有和其他信号有任何的联系,总感觉和action-target模式没什么区别,觉得这偏离了响应式编程的本意,或许这样说有点极端。

倒计时功能

先说说很常用的验证码倒计时功能,用RAC来实现几乎一气呵成 验证码 点击按钮可以重新开始倒计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  @weakify(self);
  RACSignal *timeSignal = [[[[[RACSignal interval:1.0f onScheduler:[RACScheduler mainThreadScheduler]] take:numberLimit] startWith:@(1)] map:^id(NSDate *date) {
      @strongify(self);
      if (number == 0) {
          [self.timeButton setTitle:@"重新发送" forState:UIControlStateNormal];
          return @YES;
      }
      else{
          self.timeButton.titleLabel.text = [NSString stringWithFormat:@"%d", number--];
          return @NO;
      }
  }] takeUntil:self.rac_willDeallocSignal];
  self.timeButton.rac_command = [[RACCommand alloc]initWithEnabled:timeSignal signalBlock:^RACSignal *(id input) {
      number = numberLimit;
      return timeSignal;
  }];
  • 利用 + (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler 方法返回一个在主线程上间隔一秒执行一次的signal ,RAC内部是利用 dispatch_source_set_timer dispatch_source_set_event_handler dispatch_resume 这一系列的GCD方法来实现的。注意这个signal 所sendnext的值是当前日期的NSDate对象,不过这个值对于整个功能是没有用处的。
  • - (instancetype)take:(NSUInteger)count 取前numberLimit次sendnext的值,相当于我们需要倒计时多久
  • 通过 - (instancetype)startWith:(id)value 修改第一次sendnext的值,并且立即sendnext这个值。理论上对于sendnext的值是不需要处理的,原因是+ (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler所返回的signal会停顿1秒才执行,利用startWith来立即开始倒计时罢了,所以说startWith:的参数值是什么不重要
  • 利用 - (instancetype)map:(id (^)(id value))block 修改sendnext最终的值返回@YES或者@NO
  • - (RACSignal *)takeUntil:(RACSignal *)signalTrigger 意思是当这个VC的即将dealloc的时候停止倒计时,RAC内部其实就是sendCompleted。
  • 最后把timeSignal和timeButton.rac_command绑定判断按钮是否可用,并且启动timeSignal。按钮被点击的时候恢复number的值,return buttonSignal来重新启动倒计时timeSignal

代替委托协议模式

如果RAC利用得当,几乎可以抛弃写自定义的委托协议。想象一下:当aVC presentViewController 到bVC,然后 dismissViewControllerAnimated bVC的时候需要在aVC中做一些事情,这时候通常用委托协议可以解决这种问题,比较麻烦。利用RAC实现就显得非常简洁明了

其中的关键方法是- (RACSignal *)rac_signalForSelector:(SEL)selector,基本使用是这样的

1
2
3
4
[[self rac_signalForSelector:@selector(dismiss:)] subscribeNext:^(id x) {
	NSLog(@"%s", __func__);
}];

1
2
3
- (IBAction)dismiss:(id)sender{
    [self dismissViewControllerAnimated:YES completion:NULL];
}

dismiss:方法被执行后信号就会被部署

信号什么时候部署可以由我们来决定,既然需要在aVC中处理一些事,那么就应该想办法在aVC中来部署信号。这时候就需要把信号作为bVC的属性

bVC.h

1
2
3
4
#import <UIKit/UIKit.h>
@interface RACDelegateViewController : UIViewController
@property (nonatomic, strong) RACSignal *delegateSignal;
@end

部署信号

aVC.m

1
2
3
4
5
6
7
8
9
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    if ([segue.destinationViewController isKindOfClass:[UINavigationController class]]) {
        UINavigationController *nav = (UINavigationController *)segue.destinationViewController;
        RACDelegateViewController *delegateVC = (RACDelegateViewController *)nav.topViewController;
        [delegateVC.delegateSignal subscribeNext:^(id x) {
            NSLog(@"%@", x);
        }];
    }
}

还有最后一步,生成delegateSignal

bVC.m

1
2
3
4
- (void)awakeFromNib{
    [super awakeFromNib];
    self.delegateSignal = [self rac_signalForSelector:@selector(dismiss:)];
}

注意:千万不要在- (void)viewDidLoad;去生成delegateSignal,这会导致在执行subscribeNext的时候delegateSignal还是空的。

还不够完美对不对,aVC中接收的数据一直都是UIBarButtonItem对象,想把bVC中的数据传递到aVC中应该如何实现。还必须在信号上做一些调整,利用 - (RACSignal *)then:(RACSignal * (^)(void))block; 这个方法功能是忽略接收者所有的next,直到信号complete返回一个新的RACSignal。看用法:

1
2
3
4
5
6
7
8
- (void)awakeFromNib{
    [super awakeFromNib];
    @weakify(self);
    self.delegateSignal = [[self rac_signalForSelector:@selector(dismiss:)] then:^RACSignal *{
        @strongify(self);
        return [RACSignal return:self.array];
    }];
}

[self rac_signalForSelector:@selector(dismiss:)]信号sendComplete的时候,执行一个block,这个block必须返回一个不为空的新的RACSignal。当然还有其他方法,比如:map这个信号:

1
2
3
4
5
6
7
8
- (void)awakeFromNib{
    [super awakeFromNib];
    @weakify(self);
    self.delegateSignal = [[self rac_signalForSelector:@selector(dismiss:)] map:^id(id value) {
        @strongify(self);
        return self.array;
    }];
}

对于需要监听协议方法的时候可以使用 - (RACSignal *)rac_signalForSelector:(SEL)selector fromProtocol:(Protocol *)protocol 具体可以看我的笔记共享


自动处理数据更新

注意:此功能只适合与rowsection不变的UITableView以及UICollectionView

前段时间看了objc中国的两篇文章更轻量的 View Controllers和整洁的 Table View 代码,也试着把 ReactiveCocoa 结合进 UITableView 中,下面是我做的一些尝试

  • KVO数据源数组
1
2
3
4
5
6
7
8
9
10
- (id)initWithCellIdentifier:(NSString *)aCellIdentifier
          configureCellBlock:(CellConfigureBlock)aConfigureCellBlock{
    self = [super init];
    if (self) {
        _cellIdentifier = aCellIdentifier;
        _configureCellBlock = [aConfigureCellBlock copy];
        _signal = RACObserve(self, dataSource);
    }
    return self;
}
  • map数据源得到单一的数据模型
1
2
3
4
5
6
7
8
9
10
11
12
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:_cellIdentifier
                                                            forIndexPath:indexPath];
    if (self.configureCellBlock) {
        @weakify(self);
        self.configureCellBlock(cell, [self.signal map:^id(NSArray *array) {
            @strongify(self);
            return [self itemAtIndexPath:indexPath];
        }]);
    }
    return cell;
}
  • VC中的用法
1
2
3
4
self.model = [[DataSourceGeneralModel alloc] initWithCellIdentifier:CelIdentifier
                                                 configureCellBlock:^(TableViewCell *cell, RACSignal *signal) {
        [cell configureCellWithSignal:signal];
    }];
  • cell中配置
1
2
3
4
5
6
7
8
- (void)configureCellWithSignal:(RACSignal *)signal{
    @weakify(self);
    [signal subscribeNext:^(Model *model) {
        @strongify(self);
        self.titleLabel.text = model.title;
        self.detailLabel.text = model.detail;
    }];
}

如果更新数据源数组,UITableView也会得到相应的更新,不用调用[self.tableView reloadData],像这样更新即可

1
self.model.dataSource = @[model1];

以上例子可以到github下载


代替dispatch_group功能

传统的dispatch_group创建使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 创建一个dispatch_group对象
static dispatch_group_t home_request_operation_completion_group() {
    static dispatch_group_t http_request_operation_completion_group;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        http_request_operation_completion_group = dispatch_group_create();
    });
    return http_request_operation_completion_group;
}
// .......
- (void)viewDidLoad{
    [self updateData];
}
- (void)updateProfit{
// 加入一个异步方法
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_async(home_request_operation_completion_group(), queue, ^{
    dispatch_group_enter(home_request_operation_completion_group());
        [self update1];
    });
// 加入另一个异步方法
    dispatch_group_async(home_request_operation_completion_group(), queue, ^{
    dispatch_group_enter(home_request_operation_completion_group());
        [self update2];
    });
// 当两个异步方法都执行完的时候调用停止下拉刷新
    dispatch_group_notify(home_request_operation_completion_group(), dispatch_get_main_queue(), ^{
        if ([self.tableView isHeaderRefreshing]) {
            [self.tableView headerEndRefreshing];
        });
};

RAC代替dispatch_group

1
2
3
4
5
6
7
- (void)updateProfit{
    [[RACSignal zip:@[[self update1], [self update2]]] subscribeNext:^(RACTuple *x) {
        @strongify(self);
        if ([self.tableView isHeaderRefreshing]) {
            [self.tableView headerEndRefreshing];
        });
    }];

当然这里需要改写[self update1][self update2]返回RACSignal对象。

+ (instancetype)zip:(id<NSFastEnumeration>)streams作用当绑定的多个signal都sendNext后再subscribeNext。

另外关于combineLatestzip区别可以看我的笔记


RAC相关博客

本文由作者按照 CC BY 4.0 进行授权

代码统计、分析工具

关于适配