初识ReactiveCocoa

作者: 与佳期 | 来源:发表于2016-03-18 00:51 被阅读2846次

原文 : 与佳期的个人博客(gonghonglou.com)

ReactiveCocoa 是一个Objective-C 框架,受 Functional Reactive Programming的启发。它提供了一系列用来组合和转换值流的API。

如果你早已熟悉了函数响应式编程或者知道ReactiveCocoa的基本前提,看看Documentation这个文件夹里的framework overview等文件来了解它是怎样在实践中工作的。

介绍

ReactiveCocoa受functional reactive programming的启发。在那些能被替换和修改的地方,RAC提供信号(由RACSignal代表)来捕获当前和将来的值而不是使用可变的变量。

一个文本框能够根据它的改变被绑定到最后一次的值,而不是使用额外的代码每秒去监控时钟和更新文本框。这点跟KVO很像,不过使用了block,而不是-observeValueForKeyPath:ofObject:change:context:

信号也可以进行异步操作,就像futures and promises。这极大的简化了异步软件中网络连接的代码。

RAC的重大优势之一就是它提供信号(signal)这种方式来统一的处理所有异步的行为,包括代理方法、block 回调、target-action 机制、通知和KVO。

这里是简单的例子:

// 当self.username改变时,打印新的名字到控制台
//
// RACObserve(self, username)创建一个新的RACSignal,当前self.username的值发生改变时,发送新值给newName
// -subscribeNext: 当信号发送值时将触发block
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

与KVO 通知不同的是信号能够进行统一的链式操作:

// 只有当名字的开头为"j"时才打印
//
// -filter 只有当block返回YES时才会创建一个新的RACSignal发送一个新值
[[RACObserve(self, username)
    filter:^(NSString *newName) {
        return [newName hasPrefix:@"j"];
    }]
    subscribeNext:^(NSString *newName) {
        NSLog(@"%@", newName);
    }];

信号也能被用来派生状态。在响应新值中RAC代替观察属性和设置其他的属性,能够在信号和运行周期内传达属性:

// 当self.password 和 self.passwordConfirmation相同时创建一个单向的binding使得self.createEnabled为true
//
// RAC() 是一个宏指令使得binding看起来nicer
// 
// +combineLatest:reduce: 建一个信号数组
// 当任一个信号的最后一个值发生改变时触发这个block,返回一个新的RACSignal,将block返回的值作为values发送出去
RAC(self, createEnabled) = [RACSignal 
    combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ] 
    reduce:^(NSString *password, NSString *passwordConfirm) {
        return @([passwordConfirm isEqualToString:password]);
    }];

信号不仅是在KVO上,还能在建立在随着时间而改变的值流上。例如,它们可以代表按钮点击:

// 当按钮被点击时打印信息
//
// RACCommand创建信号去表示UI行为。例如,每一个信号可以表示一个按钮的点击、与它相关联的附加工作
//
// -rac_command是封装的NSButton方法. 当按钮被点击时将发送到该命令
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
    NSLog(@"button was pressed!");
    return [RACSignal empty];
}];

或者是异步网络操作:

// 连接"Log in"按钮给网络登录
//
// 当登录命令执行时运行block,开始登录进度
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
    // 假设当网络请求完成时 -logIn 方法返回一个信号发送一个value
    return [client logIn];
}];

// -executionSignals 每次执行该命令时,这个方法返回一个信号,包括以前的block返回的信号
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
    // 成功登录时打印信息
    [loginSignal subscribeCompleted:^{
        NSLog(@"Logged in successfully!");
    }];
}];

// 按钮被点击时执行登录命令
self.loginButton.rac_command = self.loginCommand;

信号也可以代表定时器,其他的UI事件,或者别的什么随时间而改变的事件。

在异步操作方面,通过链接和转换信号可以建立更复杂的行为。在一组完整的操作之后更简单的来执行工作:

// 执行2个网络操作,当它们都完成时打印信息到控制台
//
// +merge: 当数组里的所有信号完成时,返回一个新的RACSignal
//
// -subscribeCompleted: 当信号完成时将执行这个block
[[RACSignal 
    merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]] 
    subscribeCompleted:^{
        NSLog(@"They're both done!");
    }];

信号可以被链接到顺序执行异步操作,而不是使用一堆block回调。通常这样简单的来使用futures and promises

// 用户登录,下载缓存信息,获取服务器信息。都完成后将信息打印到控制台
//
// 假设登录之后 -logInUser 方法返回一个信号
//
// -flattenMap: 当信号发送一个value时触发这个block
// 并且返回一个新的RACSignal来整合从block返回的所有的信号到一个单一信号中
[[[[client 
    logInUser] 
    flattenMap:^(User *user) {
        // 下载缓存信息,给用户返回一个信号
        return [client loadCachedMessagesForUser:user];
    }]
    flattenMap:^(NSArray *messages) {
        // Return a signal that fetches any remaining messages.
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeNext:^(NSArray *newMessages) {
        NSLog(@"New messages: %@", newMessages);
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

RAC甚至可以简单的建立在一个异步操作的结果上:

// 创建一个单向的binding,让 self.imageView.image 来放置下载下来的user的头像
//
// 假设 -fetchUserWithUsername: 方法返回一个信号发送给user
//
// -deliverOn: 创建新的信号在其他的队列中进行他们的工作
// 在这个例子中,此方法被用来将工作转移到后台队列和回到主线程
//
// -map: 每个user调用这个block,获取并且返回一个新的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的示范操作,但是它并不能说明RAC为什么如此强大。
更多示例代码参见C-41GroceryList,这些是使用ReactiveCocoa编写的iOS APP。在这个文件夹Documentation中可以查到更多的关于RAC的信息。

使用ReactiveCocoa

乍一看ReactiveCocoa是非常抽象的,很难理解该怎样将它应用到具体的问题上。

这有一些示例来展示RAC的优势

处理异步或事件驱动的数据源

许多Cocoa编程的重点是对用户事件的反应或应用状态的变化。处理这些事件的代码很快变得非常复杂的就像意大利面一样,伴随着许多回调函数和状态变量处理顺序的问题。

表面上看起来模式不同,比如UI回调,网络响应和KVO通知,实际上有很多共同之处。RACSignal统一了所有的这些不同的API,使他们可以组合在一起,并以同样的方式操纵。

例如这样的代码:

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

在ReactiveCocoa中可以这样简单的实现:

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

并行独立工作

与独立的数据集合并行工作,然后将它们合并成一个non-trivial函数到Cocoa,并经常涉及大量的同步:

objc
__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];

上面的代码可以用简单的合成信号来清理和优化:

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

简化collection转换

高亮命令函数比如 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 collection在统一的和声明的方式下被操作:

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

后记

RAC学习资料

相关文章

  • 初识ReactiveCocoa

    今天主要分享内容: ReactiveCocoa简单介绍 响应式编程和函数编程的概述 RACSignal 信号 RA...

  • 初识ReactiveCocoa

    原文 : 与佳期的个人博客(gonghonglou.com) ReactiveCocoa 是一个Objective...

  • ReactiveCocoa 初识

    简单介绍: ReactiveCocoa 是近年来比较黑科技的开源框架,但是学习路线比较陡峭,现在已经更新到5.0并...

  • ReactiveCocoa 初识(一)

        入职有一段时间了,没有开发任务,白天就看看博客写写自己的东西,晚上看世界杯,简直不要太爽,就是天台有点挤了...

  • ReactiveCocoa初识篇

    关于ReactiveCocoa ReactiveCocoa是iOS环境下的一个函数式响应式编程框架。函数式响应式编...

  • ReactiveCocoa--初识RAC

    RAC是什么?RAC — ReactiveCocoa(RAC) Github 一个开源框架!!RAC — 函数响...

  • 探究ReactiveCocoa 底层KVO封装流程

    一、对比原生KVO,初识ReactiveCocoa的KVO 我们先来看一段代码,通过触屏来动态修改视图背景色 从上...

  • iOS-ReactiveCocoa使用之RACCommand

    前言 前几天开始研究Cocoa的第三方编程框架ReactiveCocoa,其使用响应式、函数式的编程思想,对于初识...

  • 初识ReactiveCocoa(一) —— OC项目集成

    先简单介绍下ReactiveCocoa ReactiveCocoa(简称RAC)是Github上一套作用于iOS应...

  • RAC自己练习下

    ReactiveCocoa使用个人总结 ReactiveCocoa简介 ReactiveCocoa(简称RAC)是...

网友评论

    本文标题:初识ReactiveCocoa

    本文链接:https://www.haomeiwen.com/subject/nliclttx.html