ReactiveCocoa - iOS开发的新框架

Feb 11th, 2014

本文为 InfoQ 中文站特供稿件,首发地址为:文章链接。如需转载,请与 InfoQ 中文站联系。

什么是 ReactiveCocoa

ReactiveCocoa(其简称为 RAC)是由 Github 开源的一个应用于 iOS 和 OS X 开发的新框架。RAC 具有函数式编程和响应式编程的特性。它主要吸取了 .Net 的 Reactive Extensions 的设计和实现。

ReactiveCocoa 试图解决什么问题

经过一段时间的研究,我认为 ReactiveCocoa 试图解决以下 3 个问题:

  1. 传统 iOS 开发过程中,状态以及状态之间依赖过多的问题

  2. 传统 MVC 架构的问题:Controller 比较复杂,可测试性差

  3. 提供统一的消息传递机制

传统 iOS 开发过程中,状态以及状态之间依赖过多的问题

我们在开发 iOS 应用时,一个界面元素的状态很可能受多个其它界面元素或后台状态的影响。

例如,在用户帐户的登录界面,通常会有 2 个输入框(分别输入帐号和密码)和一个登录按钮。如果我们要加入一个限制条件:当用户输入完帐号和密码,并且登录的网络请求还未发出时,确定按钮才可以点 击。通常情况下,我们需要监听这两个输入框的状态变化以及登录的网络请求状态,然后修改另一个控件的enabled状态。

传统的写法如下(该示例代码修改自 ReactiveCocoa 官网 ) :

12345678910111213141516171819202122232425262728293031
static void *ObservationContext = &ObservationContext;- (void)viewDidLoad {    [super viewDidLoad];    [LoginManager.sharedManager addObserver:self                                 forKeyPath:@"loggingIn"                                    options:NSKeyValueObservingOptionInitial                                    context:&ObservationContext];    [self.usernameTextField addTarget:self action:@selector(updateLogInButton)                     forControlEvents:UIControlEventEditingChanged];    [self.passwordTextField addTarget:self action:@selector(updateLogInButton)                     forControlEvents:UIControlEventEditingChanged];}- (void)updateLogInButton {    BOOL textFieldsNonEmpty = self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0;    BOOL readyToLogIn = !LoginManager.sharedManager.isLoggingIn && !self.loggedIn;    self.logInButton.enabled = textFieldsNonEmpty && readyToLogIn;}- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object                        change:(NSDictionary *)change context:(void *)context {    if (context == ObservationContext) {        [self updateLogInButton];    } else {        [super observeValueForKeyPath:keyPath ofObject:object                               change:change context:context];    }}

RAC 通过引入信号(Signal)的概念,来代替传统 iOS 开发中对于控件状态变化检查的代理(delegate)模式或 target-action 模式。因为 RAC 的信号是可以组合(combine)的,所以可以轻松地构造出另一个新的信号出来,然后将按钮的enabled状态与新的信号绑定。如下所示:

123456789
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 之后,以前散落在action-target或 KVO 的回调函数中的判断逻辑被统一到了一起,从而使得登录按钮的enabled状态被更加清晰地表达了出来。

除了组合(combine)之外,RAC 的信号还支持链式(chaining)和过滤(filter),以方便将信号进行进一步处理。

试图解决 MVC 框架的问题

对于传统的 Model-View-Controller 的框架,Controller 很容易变得比较庞大和复杂。由于 Controller 承担了 Model 和 View 之间的桥梁作用,所以 Controller 常常与对应的 View 和 Model 的耦合度非常高,这同时也造成对其做单元测试非常不容易,对 iOS 工程的单元测试大多都只在一些工具类或与界面无关的逻辑类中进行。

RAC 的信号机制很容易将某一个 Model 变量的变化与界面关联,所以非常容易应用 Model-View-ViewModel 框架。通过引入 ViewModel 层,然后用 RAC 将 ViewModel 与 View 关联,View 层的变化可以直接响应 ViewModel 层的变化,这使得 Controller 变得更加简单,由于 View 不再与 Model 绑定,也增加了 View 的可重用性。

因为引入了 ViewModel 层,所以单元测试可以在 ViewModel 层进行,iOS 工程的可测试性也大大增强了。InfoQ 也曾撰文介绍过 MVVM:《MVVM 启示录》

统一消息传递机制

iOS 开发中有着各种消息传递机制,包括 KVO、Notification、delegation、block 以及 target-action 方式。各种消息传递机制使得开发者在做具体选择时感到困惑,例如在 objc.io 上就有 专门撰文破船的翻译 ),介绍各种消息传递机制之间的差异性。

RAC 将传统的 UI 控件事件进行了封装,使得以上各种消息传递机制都可以用 RAC 来完成。示例代码如下:

123456789101112131415161718192021222324
// KVO[RACObserve(self, username) subscribeNext:^(id x) {    NSLog(@" 成员变量 username 被修改成了:%@", x);}];// target-actionself.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {    NSLog(@" 按钮被点击 ");    return [RACSignal empty];}];// Notification[[[NSNotificationCenter defaultCenter]    rac_addObserverForName:UIKeyboardDidChangeFrameNotification                    object:nil]    subscribeNext:^(id x) {        NSLog(@" 键盘 Frame 改变 ");    }];// Delegate[[self rac_signalForSelector:@selector(viewWillAppear:)] subscribeNext:^(id x) {    debugLog(@"viewWillAppear 方法被调用 %@", x);}];

RAC 的RACSignal 类也提供了createSignal方法来让用户创建自定义的信号,如下代码创建了一个下载指定网站内容的信号。

12345678910111213141516171819
-(RACSignal *)urlResults {    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {        NSError *error;        NSString *result = [NSString stringWithContentsOfURL:[NSURL URLWithString:@"http://www.devtang.com"]                                                    encoding:NSUTF8StringEncoding                                                       error:&error];        NSLog(@"download");        if (!result) {            [subscriber sendError:error];        } else {            [subscriber sendNext:result];            [subscriber sendCompleted];        }        return [RACDisposable disposableWithBlock:^{            NSLog(@"clean up");        }];    }];}

如何使用 ReactiveCocoa

ReactiveCocoa 可以在 iOS 和 OS X 的应用开发中使用,对于 iOS 开发者,可以将 RAC 源码 下载编译后,使用编译好的libReactiveCocoa-iOS.a文件。

开发者也可以用 CocoaPods 来设置目标工程对 ReactiveCocoa 的依赖,只需要编辑 Podfile 文件,增加如下内容即可:

1
pod 'ReactiveCocoa'

ReactiveCocoa 的特点

RAC 在应用中大量使用了 block,由于 Objective-C 语言的内存管理是基于 引用计数 的,为了避免循环引用问题,在 block 中如果要引用 self,需要使用@weakify(self)@strongify(self)来避免强引用。另外,在使用时应该注意 block 的嵌套层数,不恰当的滥用多层嵌套 block 可能给程序的可维护性带来灾难。

RAC 的编程方式和传统的 MVC 方式差异巨大,所以需要较长的学习时间。并且,业界内对于 RAC 并没有广泛应用,这造成可供参考的项目和教程比较欠缺。 另外,RAC 项目本身也还在快速演进当中,1.x 版本和 2.x 版本 API 改动了许多,3.0 版本也正在快速开发中,对它的使用也需要考虑后期的升级维护问题。

作为一个 iOS 开发领域的新开源框架,ReactiveCocoa 带来了函数式编程和响应式编程的思想,值得大家关注并且学习。