Reactive Cocoa 之旅

我是前言


这是我在简书上的第一篇文章,目的是为了记录一些学习过的知识,以供日后复习。当然,如果本人的一些文章能够帮助到一些人更快、更好的完成工作或者知识扩充,本人感到非常荣幸。之前也想过写一些东西,不过因为种种原因,未能执行,本人会抽出时间回顾和修正文章中的各种问题,如果有什么疑问或者纰漏,欢迎指正。

关于本文


本文主要是学习raywenderlich的Reactive Cocoa tutorial系列教程,并加以个人理解。作者Colin Eberhardt有不少好的教程值得大家学习,话说raywenderlich真心是一个不错的网站,希望没有听说过或者听说过没有学习过的小伙伴们进去转一转,说不定会有所收获。废话不多所,进入主题。

Whats Reactive Cocoa?

Reactive Cocoa(也叫做RAC,下文统称RAC)是一个支持FRP(函数响应式编程)的框架,其灵感来自这篇博客。作为iOSer,我们写的每一行代码都是为了得到一个输出,比如说一个按钮点击之后,处理相应的事件;一个UISearchBar的文本改变,提示不同的信息;用户下拉刷新,获取网络数据以展示...所有的事件都是为了得到一个输出(结果)... 简单输入输出,可能像“Hello world”一样,而复杂的输出,可能要做一系列的操作之后,得到一系列的结果。

Q:在Cocoa的世界中,为了得到这个(些)结果,我们都采取了什么方式呢?

A:Target Action;Blocks;Delegations;Notifications;KVO;Threads and so on..

如果设计模式没用好,架构没弄好,那么我们的代码常常看起来就像是面条一样,难懂又难看。在RAC的世界中,以事件流(用Signal、SignalProductor来表示)的形式,组合和转换信号,最终得到我们想要的输出,代码大统一。

Usage

目前的RAC支持OC和Swift两个版本,前一段时间写了一段swift,不过现在的swift像外挂一样,打出来的IPA包比OC的大很多很多,所以在这里,暂时不提swift版本,日后有机会回来补上。

Round 1 UIControl Target Actions

需求来了:那个xxx啊,我们开个会,关于这个注册功能的。balabalabala...大概设计成这样

简陋的原型

界面比较简单,一个常规的注册页面。要求:用户名长度必须大于3,密码长度大于5,否则textField背景色为黄色,正确则为白色;密码框内的内容要和确认密码框的内容一致,否则确认密码框的背景色为黄色;若上述条件都满足,注册按钮可以点击,否则不可点击。

规则简单,也很合理,那么我们需要怎么做呢?

这个简单,看我的:

方案1(With out RAC):

Storyboard中大概这样:


Storyboard
#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, weak, nullable) IBOutlet UITextField *userNameField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *passwordField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *confirmField;
@property (nonatomic, weak, nullable) IBOutlet UIButton *signUpButton;

@end

@implementation ViewController

- (IBAction)didChangedUserNameFieldEditing:(id)sender {
  self.userNameField.backgroundColor = [self isValidUserName] ? [UIColor whiteColor] : [UIColor yellowColor];
  self.signUpButton.enabled = [self shouldSignUp];
}

- (IBAction)didChangedPasswordFieldEditing:(id)sender {
  self.passwordField.backgroundColor = [self isValidPassword] ? [UIColor whiteColor] : [UIColor yellowColor];
  self.confirmField.backgroundColor = [self isValidConfirm] ? [UIColor whiteColor] : [UIColor yellowColor];
  self.signUpButton.enabled = [self shouldSignUp];
}

- (IBAction)didChangedConfirmFieldEditing:(id)sender {
  self.confirmField.backgroundColor = [self isValidConfirm] ? [UIColor whiteColor] : [UIColor yellowColor];
  self.signUpButton.enabled = [self shouldSignUp];
}

- (BOOL)isValidUserName {
  return self.userNameField.text.length > 3;
}

- (BOOL)isValidPassword {
  return self.passwordField.text.length > 5;
}

- (BOOL)isValidConfirm {
  return [self isValidPassword] && [self.confirmField.text isEqualToString:self.passwordField.text];
}

- (BOOL)shouldSignUp {
  return [self isValidUserName] && [self isValidPassword] && [self isValidConfirm];
}


@end

思路很简单:

  1. userNameField逻辑最简单,根据用户名的长度,修改其输入框背景颜色,判断注册按钮的enable状态,对其更新;
  2. 用户输入的密码如果符合规则,修改passwordField背景色。同时,如果confirmField符合规则,更新confirmField的背景色,判断注册按钮的enable状态,对其更新;
  3. 判断confirmField的文字,修改其背景色,判断注册按钮的enable状态,对其更新;
  4. 点击signUpButton,获得龙虾一只。

可以发现,在每一个TextField文本改变的时候,我们都要去判断并更新signUpButton的enable状态,并且在编辑passwordField文本的时候,判断并更新confirmField的背景色;

来看看如果使用RAC怎么搞呢?

方案2 (With in RAC) :

#import "ViewController.h"
#import <ReactiveCocoa/ReactiveCocoa.h>

@interface ViewController ()

@property (nonatomic, weak, nullable) IBOutlet UITextField *userNameField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *passwordField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *confirmField;
@property (nonatomic, weak, nullable) IBOutlet UIButton *signUpButton;

@end

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  RACSignal *userNameSignal = [self.userNameField.rac_textSignal map:^id(NSString *text) {
return @(isValidInput(text, 3));
  }];
  RACSignal *passwordSignal = [self.passwordField.rac_textSignal map:^id(NSString *text) {
return @(isValidInput(text, 5));
  }];

  RACSignal *confirmSignal = [RACSignal combineLatest:@[passwordSignal, self.confirmField.rac_textSignal] reduce:^id(NSNumber *passwordValid, NSString *text) {
return @([text isEqualToString:self.passwordField.text] && passwordValid.boolValue);
  }];

  [userNameSignal subscribeNext:^(NSNumber *valid) {
self.userNameField.backgroundColor = colorWithFlag(valid.boolValue);
  }];
  [passwordSignal subscribeNext:^(NSNumber *valid) {
self.passwordField.backgroundColor = colorWithFlag(valid.boolValue);
  }];
  [confirmSignal subscribeNext:^(NSNumber *flag) {
  self.confirmField.backgroundColor = colorWithFlag(flag.boolValue);
  }];

  [[RACSignal
   combineLatest:@[userNameSignal, passwordSignal, confirmSignal] reduce:^id(NSNumber *userNameValid, NSNumber *passwordValid, NSNumber *confirmValid) {
return @(userNameValid.boolValue && passwordValid.boolValue && confirmValid.boolValue);
  }] subscribeNext:^(NSNumber *allValid) {
self.signUpButton.enabled = allValid.boolValue;
  }];

  [self.signUpButton rac_signalForControlEvents:UIControlEventTouchUpInside];
}

BOOL isValidInput(NSString *input, NSUInteger givenLength) {
  return input.length > givenLength;
}

UIColor * colorWithFlag(BOOL flag) {
  return flag ? [UIColor whiteColor] : [UIColor yellowColor];
}

@end

可以看到,用了RAC之后,我们不需要再去写action,或者IBOutlet,通篇都是一些Signal之类的东西。我大概的思路是这样的:

  1. 观察userNameField的rac_textSignal(RAC对许多类都有其不同的Signal,如果感兴趣可以去看一看),textSignal、textSignal,顾名思义,一个文本事件流,发出的信号是一个NSString *类型的对象,然后通过map:方法将信号转换成一个BOOL含义的NSNumber标识(我们发出、转换或者合并的信号量都是NSObject类型,如果需要返回基本类型,例如此处的BOOL类型,需要将基本类型升级成对象,例如此处的NSNumber *),然后通过subscirbeNext:方法订阅(个人理解为接收一个事件流,事件流发出的信号,在订阅期间可以接收到)这个信号,如果接收到的信号为真,代表合法的用户名,改变userNameField背景色为白色,否则背景色为黄色。passwordField同理;
  2. confirmField只有在密码合法并且其文本与passwordField的文本相同时,才改变其颜色为白色,否则为黄色,用RAC该怎么处理呢?在这里,我选择了combine(组合)的方式。
  • 将confirmField.rac_textSignal与passwordSignal通过combineLatest:方法组合成一个新的事件流confirmSignal,也就是说,用户每次编辑passwordField或者confirmField的时候,我们都能接收到confirmSignal发出BOOL含义的NSNumber *信号,然后订阅这个事件流,通过信号来更新其背景色;
  1. 那么更新signUpButton的逻辑同上,将三个事件流组合成一个新的事件流,订阅事件流,根据信号来更新enable状态;
  2. 事件流的处理到此为止,接下来,我们要点击注册按钮的时候,push一个页面,得到我们想要的龙虾,既然不使用target action的方式,那么RAC给我们提供了一个 [UIButton rac_signalForControlEvents:]方法来注册一个事件流,这里我们并没有对其处理,所以写成
    [self.signUpButton rac_signalForControlEvents:UIControlEventTouchUpInside];

#######个人理解:我认为,RAC这样写的好处,在订阅每一个事件流的时候,只处理这一个事件,不做多余的操作和判断;当多个事件决定同一个结果时,可以将事件流组合,而不会将逻辑拆分到各个action、delegate..中;统一我们的各种事件处理;更好的支持函数式编程;提高了程序的可读性和可维护性,更多的好处期待大家一起来发掘。

尾声咯

差不多逻辑就是这样了,但是我们并没有处理block的retain-cycle,RAC提供了两个很有意思的macro: @weakify(), @strongify(),会大大帮助我们减少代码量,相信如果没有这两个宏,RAC写起来也会很难看,可以自行Google。

还有一件事,每次写这句话,就好像<成龙历险记>中的老爹...
我们的RAC推荐这样的写法
self.userNameField.rac_textSignal

__filter: ...(这个是干嘛的,自己看看呗,这个玩意儿可以用来简化我们的代码的)

__map: ....

__reduce: ...

__subscribeNext:... (PS: 前面的双下划线代表空格,第一次用markdown,下班时间,凭记忆用的还是..请见谅)

__...

关于第一部分差不多就说这么多了,如果有时间第二部分一定尽快奉上
(由于之前的2B行为,整理了一天git,项目代码放到这里,感兴趣的可以看看)。
I'm Chris, an iOSer. 欢迎讨论,微博@叫Chris真难。:)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容