响应式编程框架ReactiveCocoa介绍与入门

来源:互联网 发布:网络二次元是什么意思 编辑:程序博客网 时间:2024/05/30 05:16

        ReactiveCocoa是Github团队开发的第三方函数式响应式编程框架,在目前市面上的很多iOS App都大量使用了这个框架。以下我简称这个框架为RAC.我下面会通过几篇博客来和大家一起学习这个强大的框架。该博客的案例代码已经上传至  https://github.com/chenyufeng1991/ReactiveCocoaDemo 。当然最好的学习方式是去阅读RAC的源码,Github上面RAC的官网地址  https://github.com/ReactiveCocoa/ReactiveCocoa 。在官网中,包含了源码,代码示例,文档。在本篇博客中,我主要是对官方文档进行翻译,并加入自己的理解与实现。这里实现的语言为OC。

 【1】ReactiveCocoa(RAC)介绍

        RAC是iOS的一个函数式响应式编程框架,而不是使用可变的变量去修改和替换原有的值。RAC提供了信号(RACSignal类)来监听当前和未来的值。通过信号的链接、组合和响应,可以让我们的代码持续的观察和更新值。我用一句话说就是:响应数据的变化。

       举个例子,我们可以绑定一个TextField输入框,只要绑定的值有改变,我们可以不添加任何额外的代码,就可以更新该输入框。工作原理类似于KVO,但是使用block块来替代重写“observeValueForKeyPath:ofObject:change:context”这个方法。信号也代表了异步操作,可以简化网络请求等异步代码。RAC的一个最主要优势就是提供了信号,统一处理了iOS中的异步行为,包括delegate,block回调,target-action机制,Notification和KVO。如下的例子:

// When self.username changes, logs the new name to the console.//// RACObserve(self, username) creates a new RACSignal that sends the current// value of self.username, then the new value whenever it changes.// -subscribeNext: will execute the block whenever the signal sends a value.[RACObserve(self, username) subscribeNext:^(NSString *newName) {    NSLog(@"%@", newName);}];

当self.username的值改变时,log中就会输出新的值。RACObserve创建了一个新的RACSignal对象,可以发送最新的值到self.username,因此值就会随时改变。当信号signal发送新的值时,-subscribeNext就会执行block块中的代码。

      但是和KVO不一样,信号可以被链起来并操作,如下代码所示:

// Only logs names that starts with "j".//// -filter returns a new RACSignal that only sends a new value when its block// returns YES.[[RACObserve(self, username)    filter:^(NSString *newName) {        return [newName hasPrefix:@"j"];    }]    subscribeNext:^(NSString *newName) {        NSLog(@"%@", newName);    }];

上面的log中只会输出包含前缀为j的字符串。-filter会返回新的RACSignal对象,可以根据block返回新的值。

      信号同样可以用来得到状态,可以很方便的给属性一个信号和操作。如下代码所示:

// Creates a one-way binding so that self.createEnabled will be// true whenever self.password and self.passwordConfirmation// are equal.//// RAC() is a macro that makes the binding look nicer.// // +combineLatest:reduce: takes an array of signals, executes the block with the// latest value from each signal whenever any of them changes, and returns a new// RACSignal that sends the return value of that block as values.RAC(self, createEnabled) = [RACSignal     combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ]     reduce:^(NSString *password, NSString *passwordConfirm) {        return @([passwordConfirm isEqualToString:password]);    }];

以上代码创建了一种新的数据绑定的方式,当self.password和self.passwordConfirmation相等的时候会返回true。RAC()是宏,可以让数据绑定看起来更加良好。+combineLatest:reduce: 是信号的数组,只要任意一个信号中的值有改变,就会用最新的值去执行block中的代码,然后返回新的RACSignal对象,用来发送新值。

      信号可以随时创建在任何值的流上,不同于KVO。举个例子,信号可以代表按钮点击:

// Logs a message whenever the button is pressed.//// RACCommand creates signals to represent UI actions. Each signal can// represent a button press, for example, and have additional work associated// with it.//// -rac_command is an addition to NSButton. The button will send itself on that// command whenever it's pressed.self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {    NSLog(@"button was pressed!");    return [RACSignal empty];}];

每当按钮点击的时候就会输出日志。RACCommand创建了一个信号表示UI事件。每一个信号可以表示一个按钮点击,并可以执行相关的操作。同样的,RACCommand也可以进行异步网络操作,如下:

// Hooks up a "Log in" button to log in over the network.//// This block will be run whenever the login command is executed, starting// the login process.self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {    // The hypothetical -logIn method returns a signal that sends a value when    // the network request finishes.    return [client logIn];}];// -executionSignals returns a signal that includes the signals returned from// the above block, one for each time the command is executed.[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {    // Log a message whenever we log in successfully.    [loginSignal subscribeCompleted:^{        NSLog(@"Logged in successfully!");    }];}];// Executes the login command when the button is pressed.self.loginButton.rac_command = self.loginCommand;

上面的代码用来连接登录按钮和网络操作。当开始登录的时候将会去执行第一个block,在block中假设的logIn方法将会在网络请求结束的时候返回一个信号。-executeSignals将会返回信号,包括了上面第一个block中的信号。

      使用信号也可以用来表示定时器,其他的UI事件,或者随着事件变化的操作。使用信号可以让复杂的异步操作通过链式和传递信号变得更加简单。当一组操作完成后信号就可以被触发,如下:

// Performs 2 network operations and logs a message to the console when they are// both completed.//// +merge: takes an array of signals and returns a new RACSignal that passes// through the values of all of the signals and completes when all of the// signals complete.//// -subscribeCompleted: will execute the block when the signal completes.[[RACSignal     merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]     subscribeCompleted:^{        NSLog(@"They're both done!");    }];

当client的两个网路请求都完成后,控制台就会打印出信息。+merge:获得信号数组,当数组中的信号都完成后,返回RACSignal对象。在异步操作中,信号可以被链式然后按序列执行,而不用使用嵌套的block回调。如下所示:

// Logs in the user, then loads any cached messages, then fetches the remaining// messages from the server. After that's all done, logs a message to the// console.//// The hypothetical -logInUser methods returns a signal that completes after// logging in.//// -flattenMap: will execute its block whenever the signal sends a value, and// returns a new RACSignal that merges all of the signals returned from the block// into a single signal.[[[[client     logInUser]     flattenMap:^(User *user) {        // Return a signal that loads cached messages for the 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.");    }];

用户登录,先加载缓存数据,然后从远程服务器抓取数据,以上操作完成后,打印log。 假设的-logInUser方法当登录完成后会返回信号。  -flattenMap:方法当信号发送一个值的时候就会去执行block,并返回一个新的RACSignal对象,该对象会合并上面所有的信号为一个单一信号。RAC可以使绑定异步操作的结果更加简单:

// Creates a one-way binding so that self.imageView.image will be set as the user's// avatar as soon as it's downloaded.//// The hypothetical -fetchUserWithUsername: method returns a signal which sends// the user.//// -deliverOn: creates new signals that will do their work on other queues. In// this example, it's used to move work to a background queue and then back to the main thread.//// -map: calls its block with each user that's fetched and returns a new// RACSignal that sends values returned from the block.RAC(self.imageView, image) = [[[[client     fetchUserWithUsername:@"joshaber"]    deliverOn:[RACScheduler scheduler]]    map:^(User *user) {        // Download the avatar (this is done on a background queue).        return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];    }]    // Now the assignment will be done on the main thread.    deliverOn:RACScheduler.mainThreadScheduler];

创建了一个绑定,当用户头像下载完成后,self.imageView.image就会被立即设置。假设的-fetchUserWithUsername:会发送一个信号。 -deliverOn:创建一个信号可以让任务在其他队列中去执行。在这个例子中,是用来让任务在后台队列执行然后切换到主线程。 

      上面简单描述了RAC可以做的一些事情,但是没有说明为什么RAC如此强大。如果想看更多的示例代码,可以查看C-41和GroceryList这两个项目,这两个项目都是用RAC来写的。


【何时使用RAC】

    当第一次看到RAC的时候,感觉非常的抽象,理解起来也非常的困难,以致于很难在具体的问题中使用。这里有一些具体在哪些情况下使用RAC的建议:

1.处理异步任务或者事件驱动数据源的时候

     大多数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];}


2.链的依赖操作

     依赖在网络请求中很常见,比如下一个请求之前要先去完成前一个请求。比如:

[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];}];
而RAC可以让这种模式变得简单,改造如下:

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

3.并行独立任务

      在并行任务中处理独立的数据集,并把它们组合成最后的结果,这样的操作往往会涉及大量的同步操作,我们常用的代码如下:

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

上面的代码可以优化为简单的组合信号,RAC后的代码如下:

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

4.简化集合操作

    高阶函数如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"];    }];

【系统要求】

   RAC要求OS X10.8+ ,iOS 8.0+.


【导入RAC】

个人推荐使用CocoaPods来导入RAC。可以查看C-41和GroceryList这两个项目,这两个项目里面已经包含了RAC.


【独立开发】

如果独立的开发RAC而不是把它集成到一个项目中,你应该要去打开ReactiveCocoa.xcworkspace 而不是.xcodeproj.


【更多资料】

     RAC是基于.NET的Reactive Extensions(Rx),很多Rx种的原理都可以应用到RAC中,下面是一些Rx的资源:

Reactive Extensions MSDN entry

Reactive Extensions for .NET Introduction

Rx - Channel 9 video

Reactive Extensions wiki

101 Rx Samples

Programming Reactive Extensions and LINQ


RAC和Rx都是一种函数式响应式编程(Functional Reactive Programming),下面是关于FRP的资源:

What is FRP? - Elm Language

What is Functions Reactive Programming - Stack Overflow

Specification for a Functional Reactive Language - Stack Overflow

Escape from Callback Hell

Principles of Reactive Programming on Coursera


本文大部分翻译自 :https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.5/README.md


1 0
原创粉丝点击