ReactiveObjC学习笔记

这篇文章的内容绝大部分翻译自github上的ReactiveObjC

ReactiveObjC

注意 : 这是ReactiveCocoa Objective-C的介绍,ReactiveCocoa的OC版本现在叫做ReactiveObjC了, ReactiveCocoa的升级版本是用Swift语言写的,想了解它的升级版本,请看ReactiveCocoa 或者 ReactiveSwift

ReactiveObjC(ReactiveCocoa 或 RAC) 是一个基于函数响应式编程思想的Objective-C框架, 它提供了各种APIs,这些 APIs 可用于组合,转换数据流。

简介

ReactiveObjC(RAC)是一个函数响应式编程框架。RAC用信号(类名为RACSignal)来代替和处理各种变量的变化和传递。

通过信号signals的传输,重新组合和响应,软件代码的编写逻辑思路将变得更清晰紧凑,有条理,而不再需要对变量的变化不断的观察更新。

例如,UITextField的文本内容(text)刚起了变化,就要立即作出响应(UITextField还没失去焦点)更新动作,我们是不会看着手表来时刻观察更新textField,类似于采取KVO措施一样重写的-observeValueForKeyPath:ofObject:change:context: ,而是通过信号Signals的block实现这一动作。

信号Signals还能够代替实现异步操作,或者是并发处理问题。这大大的简化了异步操作(如网络)的代码。

RAC的主要好处是它提供了一个信号Signal,来统一处理Cocoa的各种行为,包括delegate-methods,block回调,target-action机制,通知和KVO等等。

这里是个简单的例子:

//当self.username 改变时,在控制台打印出新的username
//宏定义RACObserve(self, username)会创建一个新的RACSignal信号,只要self.username的值有新变化,信号就会发送传递self.username新的值
//当信号signal传送一个数据时,-subscribeNext:将执行block里的语句
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

但与KVO通知模式不同,信号signals是可以被串联起来进行操作:

// 只打印以'j'开头的username.
//当-filter的block返回YES时,-filter将会返回一个新的RACSigal信号,这个信号只传送username新的值
[[RACObserve(self, username)
    filter:^(NSString *newName) {
        return [newName hasPrefix:@"j"];
    }]
    subscribeNext:^(NSString *newName) {
        NSLog(@"%@", newName);
    }];

Signals 也能够用于派生出新的东西,RAC通过信号传送和信号操作,令属性重新赋值成为可能(不是通过传统的监听属性和为属性设置其它值来响应属性的变化):

// 创建一个单向绑定,当self.password与self.passwordConfirmation相等时,self.createEnabled将被赋值为true
// RAC()是一个让绑定看起来更好的宏
// +combineLatest:reduce:接收一个存有信号的数组,当数组中任一信号更新值时,block就会被执行并会以block的返回值作为信号的传递因子创建新的信号RACSignal并返回此信号
RAC(self, createEnabled) = [RACSignal
    combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ]
    reduce:^(NSString *password, NSString *passwordConfirm) {
        return @([passwordConfirm isEqualToString:password]);
    }];

不仅是KVO可以,信号Signals也可以基于任何数据流而被创建。比方说,信号可以表示button按压事件

//只要button被按压了,就打印信息
//
//RACCommand类会创建信号来表示UI控件的动作事件,比如说,信号可以表示button的一次按压事件或者一些与button相关的其它事件,当事件发生时,signalBlock将会被执行并返回
//
//-rac_command 除了用在Button外,还可以用在其它UI控件。当button被按压时,button将向自己发送这个指令(command)

self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
    NSLog(@"button was pressed!");
    return [RACSignal empty];
}];

或者是异步网络操作:


//点击'Log in'按钮,通过网络请求登录
//当登录指令被执行开始登录操作时,这个block将被运行,这个block返回一个RACSignal
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
    //假设:-logIn 方法r 返回值是一个信号(这个信号能够发送一个数据当网络请求结束时)
    return [client logIn];
}];

//-executionSignals会返回一个信号,这个信号取自上面Signalblock的每次运行返回的值
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
    //登录成功后就打印
    [loginSignal subscribeCompleted:^{
        NSLog(@"Logged in successfully!");
    }];
}];

//button被按压,将执行登录事件指令
self.loginButton.rac_command = self.loginCommand;

Signals还可以表示时钟或其它UI事件,或者任何随时间发生改变的事件。

通过节节紧扣的链式编程和传送这些信号,让异步操作处理更多复杂的事件成为可能。在一系列动作(数据请求,验证,格式化等)完成后,紧接的动作会很容易地被触发。

//执行2个网络任务并在控制台打印信息,当2个网络任务都完成时。
//+merge类方法持有一个信号数组并返回一个新的RACSignal信号。当数组中所有信号都完成时,这个新信号会传递数组中所有信号包裹的数据。
//当新信号结束传递时,-subscribeCompleted将执行block
[[RACSignal
    merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
    subscribeCompleted:^{
        NSLog(@"They're both done!");
    }];

Signals 可以不通过嵌套用作回调的block来顺序地执行异步操作,这与同步处理有相似之处

//用户进行登录,加载缓存信息,然后从服务器拉取余下的信息。等这些动作完成后,在控制台打印信息
//假设-logInUser 方法在登录完成后返回一个signal
//当信号发送一个数据时,-flattenMap:的block将被执行,并在block执行后-flattenMap:返回一个新的信号RACSignal,这个信号将block返回的全部signal都并入进来
[[[[client
    logInUser]
    flattenMap:^(User *user) {
        //返回一个信号,这个信号包裹用户加载缓存的信息
        return [client loadCachedMessagesForUser:user];
    }]
    flattenMap:^(NSArray *messages) {
        //返回一个信号,这个信号包裹从网络上拉取剩余的信号
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeNext:^(NSArray *newMessages) {
        NSLog(@"New messages: %@", newMessages);
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

甚至,RAC可以很容易地绑定异步操作的结果:

//创建一个单向绑定,目的是:只要图片下载完成就把它设成用户的头像图片
//假设:--fetchUserWithUsername:方法返回一个信号,这个信号用于传送user对象
//-deliverOn:会创建新的信号,这些信号将工作在后台队列。在这个例子中,deliverOn方法将任务搬到后台队列去工作,然后返回到主线程
//-map:传参user给block并调用它,-map:执行结后会返回一个新的RACSignal,这个信号传送的数据是来自block的返回值

RAC(self.imageView, image) = [[[[client
    fetchUserWithUsername:@"joshaber"]
    deliverOn:[RACScheduler scheduler]]
    map:^(User *user) {
    //下载图像数据(这个任务在后台队列执行)
        return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
    }]
    // 此时,任务运行在主线程
    deliverOn:RACScheduler.mainThreadScheduler];

想看看真正使用RAC创建的项目?请check out C-41GroceryList, 它们都是用ReactiveObjC链式响应编程框架写的iOS app。

当你要使用ReactiveObjc时

粗略地回头看看上面的说明,ReactiveObjC 真的是挺抽象的,而且它让人感觉会很困难,如果把它应用在具体的问题上。

处理异步任务或事件驱动数据更新

在Cocoa框架下编程,大多数都会关注如何响应用户交互或者改变app的状态(更新页面数据或其它),这让处理这些事件的代码很快变得像意大利一样,乱成一团并且很复杂,因为这些代码会用到大量的回调和状态变量去handle问题。

RAC的编程模式在表面上看,跟UI callbacks,网络请求响应和KVO通知很不一样,实际上它们是极大地相同的。RACSignal信号只不过把它们不同的APIs统一了起来,目的是让他们变得可组合且能够以相同的方式来操作。

例如,下面的代码:

static void *ObservationContext = &ObservationContext;

- (void)viewDidLoad {
    [super viewDidLoad];

    [LoginManager.sharedManager addObserver:self forKeyPath:@"loggingIn" options:NSKeyValueObservingOptionInitial context:&ObservationContext];
    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(loggedOut:) name:UserDidLogOutNotification object:LoginManager.sharedManager];

    [self.usernameTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged];
    [self.passwordTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged];
    [self.logInButton addTarget:self action:@selector(logInPressed:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)dealloc {
    [LoginManager.sharedManager removeObserver:self forKeyPath:@"loggingIn" context:ObservationContext];
    [NSNotificationCenter.defaultCenter removeObserver:self];
}

- (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;
}

- (IBAction)logInPressed:(UIButton *)sender {
    [[LoginManager sharedManager]
        logInWithUsername:self.usernameTextField.text
        password:self.passwordTextField.text
        success:^{
            self.loggedIn = YES;
        } failure:^(NSError *error) {
            [self presentError:error];
        }];
}

- (void)loggedOut:(NSNotification *)notification {
    self.loggedIn = NO;
}

- (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写成:

- (void)viewDidLoad {
    [super viewDidLoad];

    @weakify(self);

    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);
        }];

    [[self.logInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) {
        @strongify(self);

        RACSignal *loginSignal = [LoginManager.sharedManager
            logInWithUsername:self.usernameTextField.text
            password:self.passwordTextField.text];

            [loginSignal subscribeError:^(NSError *error) {
                @strongify(self);
                [self presentError:error];
            } completed:^{
                @strongify(self);
                self.loggedIn = YES;
            }];
    }];

    RAC(self, loggedIn) = [[NSNotificationCenter.defaultCenter
        rac_addObserverForName:UserDidLogOutNotification object:nil]
        mapReplace:@NO];
}

串联相关的操作(下一任务需要上一任务的执行结果)

在网络请求中,相互关联的事件是非常普遍的,下一请求需要在上一请求完成后才能被发起,像如下:

[client logInWithSuccess:^{
    [client loadCachedMessagesWithSuccess:^(NSArray *messages) {
        [client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) {
            NSLog(@"Fetched all messages.");
        } failure:^(NSError *error) {
            [self presentError:error];
        }];
    } failure:^(NSError *error) {
        [self presentError:error];
    }];
} failure:^(NSError *error) {
    [self presentError:error];
}];

ReactiveObjC让这种模式异常简洁:

[[[[client logIn]
    then:^{
        return [client loadCachedMessages];
    }]
    flattenMap:^(NSArray *messages) {
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeError:^(NSError *error) {
        [self presentError:error];
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

平行不相关的任务(并发任务,各自执行各自的)

把几个并发任务的执行结果(数据)归并成最终的结果,这在Cocoa编程中是非常重要的,而且通常牵扯到很多同步问题:

__block NSArray *databaseObjects;
__block NSArray *fileContents;

NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
NSBlockOperation *databaseOperation = [NSBlockOperation blockOperationWithBlock:^{
    databaseObjects = [databaseClient fetchObjectsMatchingPredicate:predicate];
}];

NSBlockOperation *filesOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSMutableArray *filesInProgress = [NSMutableArray array];
    for (NSString *path in files) {
        [filesInProgress addObject:[NSData dataWithContentsOfFile:path]];
    }

    fileContents = [filesInProgress copy];
}];

NSBlockOperation *finishOperation = [NSBlockOperation blockOperationWithBlock:^{
    [self finishProcessingDatabaseObjects:databaseObjects fileContents:fileContents];
    NSLog(@"Done processing");
}];

[finishOperation addDependency:databaseOperation];
[finishOperation addDependency:filesOperation];
[backgroundQueue addOperation:databaseOperation];
[backgroundQueue addOperation:filesOperation];
[backgroundQueue addOperation:finishOperation];

上面的代码可以通过简单地组合signal信号来清减和优化:

RACSignal *databaseSignal = [[databaseClient
    fetchObjectsMatchingPredicate:predicate]
    subscribeOn:[RACScheduler scheduler]];

RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
    NSMutableArray *filesInProgress = [NSMutableArray array];
    for (NSString *path in files) {
        [filesInProgress addObject:[NSData dataWithContentsOfFile:path]];
    }

    [subscriber sendNext:[filesInProgress copy]];
    [subscriber sendCompleted];
}];

[[RACSignal
    combineLatest:@[ databaseSignal, fileSignal ]
    reduce:^ id (NSArray *databaseObjects, NSArray *fileContents) {
        [self finishProcessingDatabaseObjects:databaseObjects fileContents:fileContents];
        return nil;
    }]
    subscribeCompleted:^{
        NSLog(@"Done processing");
    }];

简化集合类的转换

高阶函数如 map,filter, fold/reduce在Foundation框架里是非常缺少的,这导致了代码要循环遍历,像这样:

NSMutableArray *results = [NSMutableArray array];
for (NSString *str in strings) {
    if (str.length < 2) {
        continue;
    }

    NSString *newString = [str stringByAppendingString:@"foobar"];
    [results addObject:newString];
}

然而,RACSequence序列能够让任何Cocoa集合类以统一和见文知义的方式来操作处理:

RACSequence *results = [[strings.rac_sequence
    filter:^ BOOL (NSString *str) {
        return str.length >= 2;
    }]
    map:^(NSString *str) {
        return [str stringByAppendingString:@"foobar"];
    }];

系统配置

ReactiveObjC 支持 OS X 10.8+ 和 iOS 8.0+

导入ReactiveObjC

这样来将RAC添加到你的App:

一:使用CocoaPods来管理RAC

前提:已经安装了CocoaPods

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

推荐阅读更多精彩内容