ReactiveCocoa学习
Posted zcube
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ReactiveCocoa学习相关的知识,希望对你有一定的参考价值。
ReactiveCocoa - iOS开发的新框架
RAC具有函数式编程和响应式编程的特性。它主要吸取了.Net的 Reactive Extensions的设计和实现。
ReactiveCocoa试图解决什么问题
传统ios开发过程中,状态以及状态之间依赖过多的问题
- 在开发iOS应用时,一个界面元素的状态很可能受多个其它界面元素或后台状态的影响。
- RAC通过引入信号(Signal)的概念,来代替传统iOS开发中对于控件状态变化检查的代理(delegate)模式或target-action模式。
- 因为RAC的信号是可以组合(combine)的,所以可以轻松地构造出另一个新的信号出来。
- 引入RAC之后,以前散落在action-target或KVO的回调函数中的判断逻辑被统一到了一起。
- 除了组合(combine)之外,RAC的信号还支持链式(chaining)和过滤(filter),以方便将信号进行进一步处理。
传统MVC架构的问题:Controller比较复杂,可测试性差
- 对于传统的Model-View-Controller的框架,Controller很容易变得比较庞大和复杂。
- Controller常常与对应的View和Model的耦合度非常高,这同时也造成对其做单元测试非常不容易。
- RAC的信号机制很容易将某一个Model变量的变化与界面关联,所以非常容易应用Model-View-ViewModel 框架。
- 通过引入ViewModel层,然后用RAC将ViewModel与View关联,View层的变化可以直接响应ViewModel层的变化,这使得Controller变得更加简单,由于View不再与Model绑定,也增加了View的可重用性。
- 因为引入了ViewModel层,所以单元测试可以在ViewModel层进行,iOS工程的可测试性也大大增强了。
提供统一的消息传递机制
- iOS开发中有着各种消息传递机制,包括KVO、Notification、delegation、block以及target-action方式。各种消息传递机制使得开发者在做具体选择时感到困惑。
- RAC将传统的UI控件事件进行了封装,使得以上各种消息传递机制都可以用RAC来完成。
ReactiveCocoa需要注意的
RAC在应用中大量使用了block,由于Objective-C语言的内存管理是基于引用计数 的,为了避免循环引用问题,在block中如果要引用self,需要使用@weakify(self)和@strongify(self)来避免强引用。
在使用时应该注意block的嵌套层数,不恰当的滥用多层嵌套block可能给程序的可维护性带来灾难。
ReactiveCocoa入门教程——第一部分
在编写iOS代码时,我们的大部分代码都是在响应一些事件:按钮点击、接收网络消息、属性变化等等。但是这些事件在代码中的表现形式却不一样:如target-action、代理方法、KVO、回调或其它。ReactiveCocoa的目的就是定义一个统一的事件处理接口,这样它们可以非常简单地进行链接、过滤和组合。
ReactiveCocoa思想和原理
ReactiveCocoa结合了一些编程模式:
- 函数式编程:利用高阶函数,即将函数作为其它函数的参数。
- 响应式编程:关注于数据流及变化的传播。
ReactiveCocoa有很多操作来控制事件流。假设你只关心超过3个字符长度的用户名,那么你可以使用filter操作来实现这个目的。把之前加在viewDidLoad中的代码更新成下面的:
[[self.usernameTextField.rac_textSignal filter:^BOOL(id value) NSString *text = value; return text.length > 3; ] subscribeNext:^(id x) NSLog(@"%@", x); ];
上面代码等价于:
RACSignal *usernameSourceSignal = self.usernameTextField.rac_textSignal; RACSignal *filteredUsername = [usernameSourceSignal filter:^BOOL(id value) NSString *text = value; return text.length > 3; ]; [filteredUsername subscribeNext:^(id x) NSLog(@"%@", x); ];
subscribeNext
定义在 RACSignal.h 文件中;filter
定义在 RACStream.h 文件中。
ReactiveCocoa框架使用category来为很多基本UIKit控件添加signal。这样你就能给控件添加订阅了,text field的rac_textSignal就是这么来的。
Objective-C 2.0中的Category语法 ,Category提供了一种比继承(inheritance)更为简洁的方法来对class进行扩展,我们可以为任何已经存在的class添加方法。
ReactiveCocoa的核心就是信号,而它不过就是事件流。
RACSignal的每个操作都会返回一个RACsignal,这在术语上叫做连贯接口(fluent interface)。这个功能可以让你直接构建管道,而不用每一步都使用本地变量。
Objective-C中的block
ReactiveCocoa大量使用block,关于在Objective-C中的声明有以下几种类型:
// As a local variable: returnType (^blockName)(parameterTypes) = ^returnType(parameters) ...; // As a property: @property (nonatomic, copy) returnType (^blockName)(parameterTypes); // As a method parameter: - (void)someMethodThatTakesABlock:(returnType (^)(parameterTypes))blockName; // As an argument to a method call: [someObject someMethodThatTakesABlock:^returnType (parameters) ...]; // As a typedef: typedef returnType (^TypeName)(parameterTypes); TypeName blockName = ^returnType(parameters) ...;
ReactiveCocoa的使用
用RAC判断用户名密码是否符合要求,并体现在界面上的一段代码:
#import "RWDummySignInService.h" #import "RWViewController.h" #import <ReactiveCocoa/ReactiveCocoa.h> @interface RWViewController () @property (weak, nonatomic) IBOutlet UITextField *usernameTextField; @property (weak, nonatomic) IBOutlet UITextField *passwordTextField; @property (weak, nonatomic) IBOutlet UIButton *signInButton; @property (weak, nonatomic) IBOutlet UILabel *signInFailureText; @property (strong, nonatomic) RWDummySignInService *signInService; @end @implementation RWViewController - (void)viewDidLoad [super viewDidLoad]; self.signInService = [RWDummySignInService new]; // initially hide the failure message self.signInFailureText.hidden = YES; // Use RAC /**************************************************** * 首先要做的就是创建一些信号,来表示用户名和密码输入框中的输入内容是否有效。 ***************************************************/ RACSignal *validUsernameSignal = [self.usernameTextField.rac_textSignal map:^id(NSString *text) return @([self isValidUsername:text]); ]; RACSignal *validPasswordSignal = [self.passwordTextField.rac_textSignal map:^id(NSString *text) return @([self isValidPassword:text]); ]; /**************************************************** * 根据条件设置文本框背景是否为黄色,优雅的做法是下面代码中使用RAC宏 [[validPasswordSignal map:^id(NSNumber *passwordValid) return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; ] subscribeNext:^(UIColor *color) self.passwordTextField.backgroundColor = color; ]; * RAC宏允许直接把信号的输出应用到对象的属性上。 * RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。 * 每次信号产生一个next事件,传递过来的值都会应用到该属性上。 ***************************************************/ RAC(self.passwordTextField, backgroundColor) = [validPasswordSignal map:^id(NSNumber *passwordValid) return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; ]; RAC(self.usernameTextField, backgroundColor) = [validUsernameSignal map:^id(NSNumber *passwordValid) return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; ]; /**************************************************** * RACsignal的这个方法可以聚合任意数量的信号,reduce block的参数和每个源信号相关。 * ReactiveCocoa有一个工具类RACBlockTrampoline,它在内部处理reduce block的可变参数。 * 实际上在ReactiveCocoa的实现中有很多隐藏的技巧,值得你去看看。 ***************************************************/ RACSignal *signUpActiveSignal = [RACSignal combineLatest:@[ validUsernameSignal, validPasswordSignal ] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid) return @([usernameValid boolValue] && [passwordValid boolValue]); ]; [signUpActiveSignal subscribeNext:^(NSNumber *signupActive) self.signInButton.enabled = [signupActive boolValue]; ]; /**************************************************** * flattenMap: 解决信号中的信号(Signal of Signals)问题。 * doNext: 添加附加操作(Adding side-effects),而且doNext: block并没有返回值。 因为它是附加操作,并不改变事件本身。 ***************************************************/ [[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] doNext:^(id x) self.signInButton.enabled = NO; self.signInFailureText.hidden = YES; ] flattenMap:^id(id x) return [self signInSignal]; ] subscribeNext:^(NSNumber *signedIn) self.signInButton.enabled = YES; BOOL success = [signedIn boolValue]; self.signInFailureText.hidden = success; if (success) // 转入下一个页面 [self performSegueWithIdentifier:@"signInSuccess" sender:self]; ]; - (BOOL)isValidUsername:(NSString *)username return username.length > 3; - (BOOL)isValidPassword:(NSString *)password return password.length > 3; /**************************************************** * 建了一个信号,使用用户名和密码登录。 * 把已有的异步API用信号的方式来表示相当简单。 ***************************************************/ - (RACSignal *)signInSignal return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) [self.signInService signInWithUsername:self.usernameTextField.text password:self.passwordTextField.text complete:^(BOOL success) [subscriber sendNext:@(success)]; [subscriber sendCompleted]; ]; return nil; ]; @end
上面代码的逻辑图如下所示:
上图展示了一些重要的概念,你可以使用ReactiveCocoa来完成一些重量级的任务。
- 分割——信号可以有很多subscriber,也就是作为很多后续步骤的源。注意上图中那个用来表示用户名和密码有效性的布尔信号,它被分割成多个,用于不同的地方。
- 聚合——多个信号可以聚合成一个新的信号,在上面的例子中,两个布尔信号聚合成了一个。实际上你可以聚合并产生任何类型的信号。
这些改动的结果就是,代码中没有用来表示两个输入框有效状态的私有属性了。这就是用响应式编程的一个关键区别,你不需要使用实例变量来追踪瞬时状态。
ReactiveCocoa入门教程——第二部分
如何取消订阅一个signal
ReactiveCocoa设计的一个目标就是支持匿名生成管道编程风格。为了支持这种模型,ReactiveCocoa自己持有全局的所有信号。如果一个signal有一个或多个订阅者,那这个signal就是活跃的。如果所有的订阅者都被移除了,那这个信号就能被销毁了。
如何取消订阅一个signal?在一个completed或者error事件之后,订阅会自动移除。还可以通过RACDisposable 手动移除订阅。这个方法并不常用到,但是还是有必要知道可以这样做。
RACSignal *backgroundColorSignal = [self.searchText.rac_textSignal map:^id(NSString *text) return [self isValidSearchText:text] ? [UIColor whiteColor] : [UIColor yellowColor]; ]; RACDisposable *subscription = [backgroundColorSignal subscribeNext:^(UIColor *color) self.searchText.backgroundColor = color; ]; // at some point in the future ... [subscription dispose];
如果你创建了一个管道,但是没有订阅(subscribeNext:)它,这个管道就不会执行,包括任何如doNext: block的附加操作。
避免循环引用
subscribeNext:block中使用了self来获取text field的引用。block会捕获并持有其作用域内的值。因此,如果self和这个信号之间存在一个强引用的话,就会造成循环引用。循环引用是否会造成问题,取决于self对象的生命周期。如果self的生命周期是整个应用运行时,比如说本例,那也就无伤大雅。但是在更复杂一些的应用中,就不是这么回事了。
为了避免潜在的循环引用,Apple的文档Working With Blocks中建议获取一个self的弱引用。比如下面这样使用的:
__weak typeof(self) weakSelf = self; // Capture the weak reference [[self.searchText.rac_textSignal map:^id(NSString *text) return [self isValidSearchText:text] ? [UIColor whiteColor] : [以上是关于ReactiveCocoa学习的主要内容,如果未能解决你的问题,请参考以下文章