ReactiveCocoa self-description: working principle and application

ReactiveCocoa self-description: working principle and application

ReactiveCocoa (RAC) is an Objective-C framework inspired by functional reactive programming.

If you are already familiar with functional reactive programming or know some of the basic premises of ReactiveCocoa, check out the Documentation folder for an overview of the framework, which contains some in-depth information about how it works.

What is ReactiveCocoa?

The ReactiveCocoa documentation is well written and covers in detail what RAC is and how it works.

If you want to learn a little more, we recommend these resources:

Introduction

When to use ReactiveCocoa

Framework Overview

Basic Operators

Header documentation

Previously answered Stack Overflow questions and GitHub issues

The rest of the Documentation folder

Functional Reactive Programming on iOS(eBook)

If you have any other questions, feel free to file an issue.

file an issue.

introduce

ReactiveCocoa is inspired by functional responsive programming. Rather than using mutable variables which are replaced and modified in-place, RAC provides signals (expressed as RACSignal) to capture current and future values.

By connecting, binding, and responding to signals, software can be written without having to continually observe and update values.

For example, a text field can be bound to its latest state even as it changes, without having to write extra code to update the text field’s state every second. It’s a bit like KVO, but it uses blocks instead of overriding -observeValueForKeyPath:ofObject:change:context:.

Signals can also represent asynchronous operations, a bit like futures and promises. This greatly simplifies asynchronous software, including network processing code.

One of the main advantages of RAC is that it provides a single, unified approach to handling asynchronous behavior, including delegate methods, blocks callbacks, target-action mechanisms, notifications, and KVO.

Here is a simple example:

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

Unlike KVO notifications, signals can be chained together and can operate simultaneously:

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

Signals can also be used to expose state. Instead of observing properties or setting other properties to reflect new values, RAC makes it possible to express properties through signals and operations :

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

Signals can be used in many places besides KVO. For example, they can also display button presses:

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

Or asynchronous network operations:

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

Signals can display timers, other UI events, or anything else related to time changes.

For asynchronous operations using signals, more complex behaviors can be achieved by connecting and changing these signals. Work can be simply triggered when a set of operations completes:

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

Signals can execute asynchronous operations sequentially instead of nested block callbacks. This is very similar to futures and promises:

  1. // Logs in the user, then loads any cached messages, then fetches the remaining  
  2. // messages from the server. After that's all done, logs a message to the  
  3. // console.  
  4. //  
  5. // The hypothetical -logInUser methods returns a signal that completes after  
  6. // logging in.  
  7. //  
  8. // -flattenMap: will execute its block whenever the signal sends a value, and  
  9. // returns a new RACSignal that merges all of the signals returned from the block  
  10. // into a single signal.  
  11. [[[[client
  12. logInUser]
  13. flattenMap:^(User *user) {
  14. // Return a signal that loads cached messages for the user.  
  15. return [client loadCachedMessagesForUser:user];
  16. }]
  17. flattenMap:^(NSArray *messages) {
  18. // Return a signal that fetches any remaining messages.  
  19. return [client fetchMessagesAfterMessage:messages.lastObject];
  20. }]
  21. subscribeNext:^(NSArray *newMessages) {
  22. NSLog(@ "New messages: %@" , newMessages);
  23. } completed:^{
  24. NSLog(@ "Fetched all messages." );
  25. }];

RAC can also easily bind the results of asynchronous operations:

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

This only describes what RAC can do, but it is difficult to explain why RAC is so powerful. Although it is difficult to explain RAC through this README, I will try to use less code, fewer templates, and better code to express it clearly.

If you want more sample code, check out C-41 or GroceryList, which are real iOS apps written with ReactiveCocoa. For more information about RAC, see the Documentation folder.

When to use ReactiveCocoa

At first glance, ReactiveCocoa is very abstract, and it might be hard to understand how to apply it to a specific problem.

Here are some common places where RAC is used.

Handling asynchronous or event-driven data sources

A lot of Cocoa programming focuses on responding to user events or changing application state. Writing code this way can quickly become a spaghetti-like mess with lots of callbacks and state variables.

This pattern looks different on the surface, like UI callbacks, network responses, and KVO notifications, but actually has a lot in common. RACSignal unifies these APIs so that they can be combined and operated in the same way.

Take the following code as an example:

  1. static   void *ObservationContext = &ObservationContext;
  2. - ( void )viewDidLoad {
  3. [ super viewDidLoad];
  4. [LoginManager.sharedManager addObserver:self forKeyPath:@ "loggingIn" options:NSKeyValueObservingOptionInitial context:&ObservationContext];
  5. [NSNotificationCenter.defaultCenter addObserver:self selector: @selector (loggedOut:) name:UserDidLogOutNotification object:LoginManager.sharedManager];
  6. [self.usernameTextField addTarget:self action: @selector (updateLogInButton) forControlEvents:UIControlEventEditingChanged];
  7. [self.passwordTextField addTarget:self action: @selector (updateLogInButton) forControlEvents:UIControlEventEditingChanged];
  8. [self.logInButton addTarget:self action: @selector (logInPressed:) forControlEvents:UIControlEventTouchUpInside];
  9. }
  10. - ( void )dealloc {
  11. [LoginManager.sharedManager removeObserver:self forKeyPath:@ "loggingIn" context:ObservationContext];
  12. [NSNotificationCenter.defaultCenter removeObserver:self];
  13. }
  14. - ( void )updateLogInButton {
  15. BOOL textFieldsNonEmpty = self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0 ;
  16. BOOL readyToLogIn = !LoginManager.sharedManager.isLoggingIn && !self.loggedIn;
  17. self.logInButton.enabled = textFieldsNonEmpty && readyToLogIn;
  18. }
  19. - (IBAction)logInPressed:(UIButton *)sender {
  20. [[LoginManager sharedManager]
  21. logInWithUsername:self.usernameTextField.text
  22. password:self.passwordTextField.text
  23. success:^{
  24. self.loggedIn = YES;
  25. } failure:^(NSError *error) {
  26. [self presentError:error];
  27. }];
  28. }
  29. - ( void )loggedOut:(NSNotification *)notification {
  30. self.loggedIn = NO;
  31. }
  32. - ( void )observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:( void *)context {
  33. if (context == ObservationContext) {
  34. [self updateLogInButton];
  35. } else {
  36. [ super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  37. }
  38. }

…expressed in RAC like this:

  1. - ( void )viewDidLoad {
  2. [ super viewDidLoad];
  3. @weakify (self);
  4. RAC(self.logInButton, enabled) = [RACSignal
  5. combineLatest:@[
  6. self.usernameTextField.rac_textSignal,
  7. self.passwordTextField.rac_textSignal,
  8. RACObserve(LoginManager.sharedManager, loggingIn),
  9. RACObserve(self, loggedIn)
  10. ] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) {
  11. return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);
  12. }];
  13. [[self.logInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) {
  14. @strongify (self);
  15. RACSignal *loginSignal = [LoginManager.sharedManager
  16. logInWithUsername:self.usernameTextField.text
  17. password:self.passwordTextField.text];
  18. [loginSignal subscribeError:^(NSError *error) {
  19. @strongify (self);
  20. [self presentError:error];
  21. } completed:^{
  22. @strongify (self);
  23. self.loggedIn = YES;
  24. }];
  25. }];
  26. RAC(self, loggedIn) = [[NSNotificationCenter.defaultCenter
  27. rac_addObserverForName:UserDidLogOutNotification object:nil]
  28. mapReplace: @NO ];
  29. }

Connection-dependent operations

Dependencies are often used in network requests. When the next network request to the server needs to be built after the previous one is completed, you can look at the following code:

  1. [client logInWithSuccess:^{
  2. [client loadCachedMessagesWithSuccess:^(NSArray *messages) {
  3. [client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) {
  4. NSLog(@ "Fetched all messages." );
  5. } failure:^(NSError *error) {
  6. [self presentError:error];
  7. }];
  8. } failure:^(NSError *error) {
  9. [self presentError:error];
  10. }];
  11. } failure:^(NSError *error) {
  12. [self presentError:error];
  13. }];

ReactiveCocoa makes this pattern particularly easy:

  1. __block NSArray *databaseObjects;
  2. __block NSArray *fileContents;
  3. NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
  4. NSBlockOperation *databaseOperation = [NSBlockOperation blockOperationWithBlock:^{
  5. databaseObjects = [databaseClient fetchObjectsMatchingPredicate:predicate];
  6. }];
  7. NSBlockOperation *filesOperation = [NSBlockOperation blockOperationWithBlock:^{
  8. NSMutableArray *filesInProgress = [NSMutableArray array];
  9. for (NSString *path in files) {
  10. [filesInProgress addObject:[NSData dataWithContentsOfFile:path]];
  11. }
  12. fileContents = [filesInProgress copy];
  13. }];
  14. NSBlockOperation *finishOperation = [NSBlockOperation blockOperationWithBlock:^{
  15. [self finishProcessingDatabaseObjects:databaseObjects fileContents:fileContents];
  16. NSLog(@ "Done processing" );
  17. }];
  18. [finishOperation addDependency:databaseOperation];
  19. [finishOperation addDependency:filesOperation];
  20. [backgroundQueue addOperation:databaseOperation];
  21. [backgroundQueue addOperation:filesOperation];
  22. [backgroundQueue addOperation:finishOperation];

The above code can be easily cleaned up and optimized using synthetic signals:

  1. RACSignal *databaseSignal = [[databaseClient
  2. fetchObjectsMatchingPredicate:predicate]
  3. subscribeOn:[RACScheduler scheduler]];
  4. RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id subscriber) {
  5. NSMutableArray *filesInProgress = [NSMutableArray array];
  6. for (NSString *path in files) {
  7. [filesInProgress addObject:[NSData dataWithContentsOfFile:path]];
  8. }
  9. [subscriber sendNext:[filesInProgress copy]];
  10. [subscriber sendCompleted];
  11. }];
  12. [[RACSignal
  13. combineLatest:@[ databaseSignal, fileSignal ]
  14. reduce:^ id (NSArray *databaseObjects, NSArray *fileContents) {
  15. [self finishProcessingDatabaseObjects:databaseObjects fileContents:fileContents];
  16. return nil;
  17. }]
  18. subscribeCompleted:^{
  19. NSLog(@ "Done processing" );
  20. }];

Simplifying collection conversions

Advanced features like map, filter, fold/reduce are sorely lacking in Foundation, leading to loop-heavy code like this:

  1. NSMutableArray *results = [NSMutableArray array];
  2. for (NSString *str in strings) {
  3. if (str.length < 2 ) {
  4. continue ;
  5. }
  6. NSString *newString = [str stringByAppendingString:@ "foobar" ];
  7. [results addObject:newString];
  8. }

RACSequence allows Cocoa collections to be manipulated in a unified way:

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

System requirements

ReactiveCocoa requires OS X 10.8+ and iOS 8.0+.

Introducing ReactiveCocoa

To add RAC to your application:

1. Add the ReactiveCocoa repository as a submodule of your application repository.

2. Run script/bootstrap from the ReactiveCocoa folder.

3. Drag ReactiveCocoa.xcodeproj into your app’s Xcode project or workspace.

4. In the "Build Phases" tab of your application target, add RAC to "Link Binary With Libraries"

On iOS, add libReactiveCocoa-iOS.a.

On OS X, add ReactiveCocoa.framework.

RAC must select "Copy Frameworks". If you don't have it, you need to select "Copy Files" and "Frameworks".

5. Add "$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include"

$(inherited) to "Header Search Paths" (this requires archive builds, but has no effect).

6. For iOS targets, add -ObjC to "Other Linker Flags" .

7. If you add RAC to a project (not a workspace), you need to add the appropriate RAC target to your application's "Target Dependencies".

If you prefer to use CocoaPods, there are some generous third-party contributed ReactiveCocoa podspecs.

To see a project using RAC, check out C-41 or GroceryList, which are real iOS apps written with ReactiveCocoa.

Independent development

If you are working with RAC in isolation rather than integrating it into another project, you will want to open ReactiveCocoa.xcworkspace instead of .xcodeproj.

More information

ReactiveCocoa is inspired by .NET's ReactiveExtensions (Rx). Some of the principles of Rx can also be used well in RAC. Here are some good Rx resources:

Reactive Extensions MSDN entry

Reactive Extensions for .NET Introduction

Rx - Channel 9 videos

Reactive Extensions wiki

101 Rx Samples

Programming Reactive Extensions and LINQ

RAC and Rx are both inspired by functional reactive programming. Here are some resources about FRP:

What is FRP? - Elm Language

What is Functional Reactive Programming - Stack Overflow

Specification for a Functional Reactive Language - Stack Overflow

Escape from Callback Hell

Principles of Reactive Programming on Coursera

<<:  Android system has the largest market share, WP is still the third

>>:  Is it really good to be a key player in a team?

Recommend

How to build a community from 0 to 1?

Communities are usually divided into two dimensio...

Do you know some little-known facts about bayberry?

If we were to talk about the most popular summer ...

How to promote user sharing and dissemination?

For internet dogs, it is easy to organize an even...

2020 Double Eleven strategy, 2020 Double Eleven activity strategy!

2020 Double Eleven strategy, 2020 Double Eleven a...

Why do we fall asleep at night and wake up in the morning?

In temperate zones, or in low-latitude areas, in ...

A complete breakdown of the promotion rhythm of e-commerce platforms

What is the pyramid theory - topic → core element...

How to promote your brand on TikTok? 5 Tips

According to InfluencerMarketingHub, TikTok has 5...

National Day brand marketing tactics, take it and you’re welcome!

National Day is coming, are you ready for your ma...

Tips for writing short videos about drama

Nowadays, short videos have become one of the mai...

How to start with data analysis to carry out SEM bidding promotion?

SEM is the abbreviation of Search Engine Marketin...

Tu Mi Character Design Issue 2 2021 [Good quality with courseware]

Tu Mi Character Design Issue 2 2021 [Good quality...