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
Chaining Signals
Once the user has (hopefully!) granted access to their Twitter accounts, the application needs to continuously monitor the changes to the search text field, in order to query twitter.
The application needs to wait for the signal that requests access to Twitter to emit its completed event, and then subscribe to the text field’s signal. The sequential chaining of different signals is a common problem, but one that ReactiveCocoa handles very gracefully.
Replace your current pipeline at the end of viewDidLoad
with the following:
[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
@strongify(self)
return self.searchText.rac_textSignal;
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
The then
method waits until a completed event is emitted, then subscribes to the signal returned by its block parameter. This effectively passes control from one signal to the next.
Note: You’ve already weakified self
for the pipeline that sits just above this one, so there is no need to precede this pipeline with a @weakify(self)
.
Note: You’ve already weakified self
for the pipeline that sits just above this one, so there is no need to precede this pipeline with a @weakify(self)
.
The then
method passes error events through. Therefore the final subscribeNext:error:
block still receives errors emitted by the initial access-requesting step.
When you build and run, then grant access, you should see the text you input into the search field logged in the console:
2014-01-04 08:16:11.444 TwitterInstant[39118:a0b] m
2014-01-04 08:16:12.276 TwitterInstant[39118:a0b] ma
2014-01-04 08:16:12.413 TwitterInstant[39118:a0b] mag
2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi
2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic
2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!
Next, add a filter
operation to the pipeline to remove any invalid search strings. In this instance, they are strings comprised of less than three characters:
[[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
@strongify(self)
return self.searchText.rac_textSignal;
}]
filter:^BOOL(NSString *text) {
@strongify(self)
return [self isValidSearchText:text];
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
Build and run again to observe the filtering in action:
2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi
2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic
2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!
Illustrating the current application pipeline graphically, it looks like this:
The application pipeline starts with the requestAccessToTwitterSignal
then switches to the rac_textSignal
. Meanwhile, next events pass through a filter and finally onto the subscription block. You can also see any error events emitted by the first step are consumed by the same subscribeNext:error:
block.
Now that you have a signal that emits the search text, it is time to use this to search Twitter! Are you having fun yet? You should be because now you’re really getting somewhere.
Searching Twitter
The Social Framework is an option to access the Twitter Search API. However, as you might expect, the Social Framework is not reactive! The next step is to wrap the required API method calls in a signal. You should be getting the hang of this process by now!
Within RWSearchFormViewController.m, add the following method:
- (SLRequest *)requestforTwitterSearchWithText:(NSString *)text {
NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/1.1/search/tweets.json"];
NSDictionary *params = @{@"q" : text};
SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter
requestMethod:SLRequestMethodGET
URL:url
parameters:params];
return request;
}
This creates a request that searches Twitter via the v1.1 REST API. The above code uses the q
search parameter to search for tweets that contain the given search string. You can read more about this search API, and other parameters that you can pass, in the Twitter API docs.
The next step is to create a signal based on this request. Within the same file, add the following method:
- (RACSignal *)signalForSearchWithText:(NSString *)text {
// 1 - define the errors
NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain
code:RWTwitterInstantErrorNoTwitterAccounts
userInfo:nil];
NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain
code:RWTwitterInstantErrorInvalidResponse
userInfo:nil];
// 2 - create the signal block
@weakify(self)
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self);
// 3 - create the request
SLRequest *request = [self requestforTwitterSearchWithText:text];
// 4 - supply a twitter account
NSArray *twitterAccounts = [self.accountStore
accountsWithAccountType:self.twitterAccountType];
if (twitterAccounts.count == 0) {
[subscriber sendError:noAccountsError];
} else {
[request setAccount:[twitterAccounts lastObject]];
// 5 - perform the request
[request performRequestWithHandler: ^(NSData *responseData,
NSHTTPURLResponse *urlResponse, NSError *error) {
if (urlResponse.statusCode == 200) {
// 6 - on success, parse the response
NSDictionary *timelineData =
[NSJSONSerialization JSONObjectWithData:responseData
options:NSJSONReadingAllowFragments
error:nil];
[subscriber sendNext:timelineData];
[subscriber sendCompleted];
}
else {
// 7 - send an error on failure
[subscriber sendError:invalidResponseError];
}
}];
}
return nil;
}];
}
Taking each step in turn:
- Initially, you need to define a couple of different errors, one to indicate the user hasn’t added any Twitter accounts to their device, and the other to indicate an error when performing the query itself.
- As before, a signal is created.
- Create a request for the given search string using the method you added in the previous step.
- Query the account store to find the first available Twitter account. If no accounts are given, an error is emitted.
- The request executes.
- In the event of a successful response (HTTP response code 200), the returned JSON data is parsed and emitted along as a next event, followed by a completed event.
- In the event of an unsuccessful response, an error event is emitted.
Now to put this new signal to use!
In the first part of this tutorial you learnt how to use flattenMap
to map each next event to a new signal that is then subscribed to. It’s time to put this to use once again. At the end of viewDidLoad
update your application pipeline by adding a flattenMap
step at the end:
[[[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
@strongify(self)
return self.searchText.rac_textSignal;
}]
filter:^BOOL(NSString *text) {
@strongify(self)
return [self isValidSearchText:text];
}]
flattenMap:^RACStream *(NSString *text) {
@strongify(self)
return [self signalForSearchWithText:text];
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
Build and run, then type some text into the search text field. Once the text is at least three characters or more in length, you should see the results of the Twitter search in the console window.
The following shows just a snippet of the kind of data you’ll see:
2014-01-05 07:42:27.697 TwitterInstant[40308:5403] {
"search_metadata" = {
"completed_in" = "0.019";
count = 15;
"max_id" = 419735546840117248;
"max_id_str" = 419735546840117248;
"next_results" = "?max_id=419734921599787007&q=asd&include_entities=1";
query = asd;
"refresh_url" = "?since_id=419735546840117248&q=asd&include_entities=1";
"since_id" = 0;
"since_id_str" = 0;
};
statuses = (
{
contributors = "<null>";
coordinates = "<null>";
"created_at" = "Sun Jan 05 07:42:07 +0000 2014";
entities = {
hashtags = ...
The signalForSearchText:
method also emits error events which the subscribeNext:error:
block consumes. You could take my word for this, but you’d probably like to test it out!
Within the simulator open up the Settings app and select your Twitter account, then delete it by tapping the Delete Account button:
If you re-run the application, it is still granted access to the user’s Twitter accounts, but there are no accounts available. As a result the signalForSearchText
method will emit an error, which will be logged:
2014-01-05 07:52:11.705 TwitterInstant[41374:1403] An error occurred: Error
Domain=TwitterInstant Code=1 "The operation couldn’t be completed. (TwitterInstant error 1.)"
The Code=1
indicates this is the RWTwitterInstantErrorNoTwitterAccounts
error. In a production application, you would want to switch on the error code and do something more meaningful than just log the result.
This illustrates an important point about error events; as soon as a signal emits an error, it falls straight-through to the error-handling block. It is an exceptional flow.
Note: Have a go at exercising the other exceptional flow when the Twitter request returns an error. Here’s a quick hint, try changing the request parameters to something invalid!
Note: Have a go at exercising the other exceptional flow when the Twitter request returns an error. Here’s a quick hint, try changing the request parameters to something invalid!