ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2
Get to grips with ReactiveCocoa in this 2-part tutorial series. Put the paradigms to one-side, and understand the practical value with work-through examples By Colin Eberhardt.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2
40 mins
Avoiding Retain Cycles
While ReactiveCocoa does a lot of clever stuff behind the scenes — which means you don’t have to worry too much about the memory management of signals — there is one important memory-related issue you do need to consider.
If you look at the reactive code you just added:
[[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
self.searchText.backgroundColor = color;
}];
The subscribeNext:
block uses self
in order to obtain a reference to the text field. Blocks capture and retain values from the enclosing scope, therefore if a strong reference exists between self
and this signal, it will result in a retain cycle. Whether this matters or not depends on the lifecycle of the self
object. If its lifetime is the duration of the application, as is the case here, it doesn’t really matter. But in more complex applications, this is rarely the case.
In order to avoid this potential retain cycle, the Apple documentation for Working With Blocks recommends capturing a weak reference to self
. With the current code you can achieve this as follows:
__weak RWSearchFormViewController *bself = self; // Capture the weak reference
[[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
bself.searchText.backgroundColor = color;
}];
In the above code bself
is a reference to self
that has been marked as __weak
in order to make it a weak reference. Notice that the subscribeNext:
block now uses the bself
variable. This doesn’t look terribly elegant!
The ReactiveCocoa framework inlcudes a little trick you can use in place of the above code. Add the following import to the top of the file:
#import "RACEXTScope.h"
Then replace the above code with the following:
@weakify(self)
[[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
@strongify(self)
self.searchText.backgroundColor = color;
}];
The @weakify
and @strongify
statements above are macros defined in the Extended Objective-C library, and they are also included in ReactiveCocoa. The @weakify
macro allows you to create shadow variables which are weak references (you can pass multiple variables if you require multiple weak references), the @strongify
macro allows you to create strong references to variables that were previously passed to @weakify
.
@weakify
and @strongify
actually do, within Xcode select Product -> Perform Action -> Preprocess “RWSearchForViewController”. This will preprocess the view controller, expand all the macros and allow you to see the final output.
One final note of caution, take care when using instance variables within blocks. These will also result in the block capturing a strong reference to self
. You can turn on a compiler warning to alert you if your code results in this problem. Search for retain within the project’s build settings to find the options indicated below:
Okay, you survived the theory, congrats! Now you’re much wiser and ready to move on to the fun part: adding some real functionality to your application!
Note: The keen-eyed readers among you who paid attention in the previous tutorial will have no doubt notice that you can remove the need for the subscribeNext:
block in the current pipeline by making use of the RAC
macro. If you spotted this, make that change and award yourself a shiny gold star!
Note: The keen-eyed readers among you who paid attention in the previous tutorial will have no doubt notice that you can remove the need for the subscribeNext:
block in the current pipeline by making use of the RAC
macro. If you spotted this, make that change and award yourself a shiny gold star!
Requesting Access to Twitter
You’re going to use the Social Framework in order to allow the TwitterInstant application to search for Tweets, and the Accounts Framework in order to grant access to Twitter. For a more detailed overview of the Social Framework, check out the chapter dedicated to this framework in iOS 6 by Tutorials.
Before you add this code, you need to input your Twitter credentials into the simulator or the iPad you’re running this app on. Open the Settings app and select the Twitter menu option, then add your credentials on the right hand side of the screen:
The starter project already has the required frameworks added, so you just need to import the headers. Within RWSearchFormViewController.m, add the following imports to the top of the file:
#import <Accounts/Accounts.h>
#import <Social/Social.h>
Just beneath the imports add the following enumeration and constant:
typedef NS_ENUM(NSInteger, RWTwitterInstantError) {
RWTwitterInstantErrorAccessDenied,
RWTwitterInstantErrorNoTwitterAccounts,
RWTwitterInstantErrorInvalidResponse
};
static NSString * const RWTwitterInstantDomain = @"TwitterInstant";
You’re going to be using these shortly to identify errors.
Further down the same file, just beneath the existing property declarations, add the following:
@property (strong, nonatomic) ACAccountStore *accountStore;
@property (strong, nonatomic) ACAccountType *twitterAccountType;
The ACAccountsStore
class provides access to the various social media accounts your device can connect to, and the ACAccountType
class represents a specific type of account.
Further down the same file, add the following to the end of viewDidLoad
:
self.accountStore = [[ACAccountStore alloc] init];
self.twitterAccountType = [self.accountStore
accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
This creates the accounts store and Twitter account identifier.
When an app requests access to a social media account, the user sees a pop-up. This is an asynchronous operation, hence it is a good candidate for wrapping in a signal in order to use it reactively!
Further down the same file, add the following method:
- (RACSignal *)requestAccessToTwitterSignal {
// 1 - define an error
NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain
code:RWTwitterInstantErrorAccessDenied
userInfo:nil];
// 2 - create the signal
@weakify(self)
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 3 - request access to twitter
@strongify(self)
[self.accountStore
requestAccessToAccountsWithType:self.twitterAccountType
options:nil
completion:^(BOOL granted, NSError *error) {
// 4 - handle the response
if (!granted) {
[subscriber sendError:accessError];
} else {
[subscriber sendNext:nil];
[subscriber sendCompleted];
}
}];
return nil;
}];
}
This method does the following:
- An error is defined, which is sent if the user refuses access.
- As per the first article, the class method
createSignal
returns an instance ofRACSignal
. - Access to Twitter is requested via the account store. At this point, the user will see a prompt asking them to grant this app access to their Twitter accounts.
- After the user grants or denies access, the signal events are emitted. If the user grants access, a next event followed by a completed are sent. If the user denies access, an error event is emitted.
If you recall from the first tutorial, a signal can emit three different event types:
- Next
- Completed
- Error
Over a signal’s lifetime, it may emit no events, one or more next events followed by either a completed event or an error event.
Finally, in order to make use of this signal, add the following to the end of viewDidLoad
:
[[self requestAccessToTwitterSignal]
subscribeNext:^(id x) {
NSLog(@"Access granted");
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
If you build and run, the following prompt should greet you::
If you tap OK, the log message in the subscribeNext:
block should appear in the console, whereas, if you tap Don’t Allow, the error block executes and logs the respective message.
The Accounts Framework remembers the decision you made. Therefore to test both paths you need to reset the simulator via the iOS Simulator -> Reset Contents and Settings … menu option. This is a bit of a pain because you also have to re-enter your Twitter credentials!