原文地址:https://www.raywenderlich.com/62796/reactivecocoa-tutorial-pt2
ReactiveCocoais a framework that allows you to useFunctional Reactive Programming(FRP) techniques within your iOS applications. With thefirst installmentof this two-part ReactiveCocoa tutorial series you learned how to replace standard actions and event handling logic with signals that emit streams of events. You also learned how to transform,split and combine these signals.
In this,thesecondpart of the series,you’re going to learn about the more advanced features of ReactiveCocoa. Including:
- The two other event types:errorandcompleted
- Throttling
- Threading
- Continuations
- …and more!
It’s time to dive in!
Twitter Instant
The application you’re going to develop throughout this tutorial is called Twitter Instant (modeled on theGoogle Instantconcept),a Twitter search application that updates search results in real-time as you type.
Thestarter projectfor this application includes the basic user interface and some of the more mundane code you’ll need to get you started. As withpart 1,you’ll need to useCocoaPodsto obtain the ReactiveCocoa framework and integrate it with your project. The starter project already includes the necessary Podfile,so open up a terminal window and execute the following command:
pod install
If it executes correctly,you should see output similar to the following:
Analyzing dependencies
Downloading dependencies
Using ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project
This should have generated a Xcode workspace,TwitterInstant.xcworkspace. Open this up in Xcode and confirm that it contains two projects:
- TwitterInstant: which is where your application logic will live
- Pods: which is where the external dependencies reside. Currently it just contains ReactiveCocoa
Build and run. The following interface will greet you:
Take a moment to familiarize yourself with the application code. It is a very simple split view controller-based app. The left-hand panel is theRWSearchFormViewController,which has a few UI controls added via the storyboard,and the search text field connected to an outlet. The right-hand panel is theRWSearchResultsViewController,which is a currently just aUITableViewController
subclass.
If you open upRWSearchFormViewController.myou can see theviewDidLoad
method locates the results view controller and assigns it to theresultsViewController
private property. The majority of your application logic is going to live withinRWSearchResultsViewController.
Validating the Search Text
The first thing you’re going to do is validate the search text to ensure it’s length is greater than two characters. This should be a pleasant refresher if you completedpart 1of this series.
WithinRWSearchFormViewController.madd the following method just belowviewDidLoad
:
- (BOOL)isValidSearchText:(NSString *)text {
return text.length > 2;
}
This simply ensures the supplied search string is longer than two characters. With such simple logic you might be asking “Why is this a separate method in the project file?”
The current logicissimple. But what if it needed to be more complex in future? With the above example,you would only make changes in one place. Furthermore,the above makes your code more expressive and it indicates why you’re checking the length of the string. We all follow good coding practices,right?
At the top of the same file,import ReactiveCocoa:
#import <ReactiveCocoa.h>
Within the same file add the following to the end ofviewDidLoad
:
[[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
self.searchText.backgroundColor = color;
}];
Wondering what that’s all about? The above code:
- Takes the search text field’s text signal
- Transforms it into a background color that indicates whether it is valid or not
- Then applies this to the text field’s
backgroundColor
property in thesubscribeNext:
block.
Build and run to observe how the text field now indicates an invalid entry with a yellow background if the current search string is too short.
Illustrated graphically,this simple reactive pipeline looks a bit like this:
Of course,you do remember this from thefirst article,right? If not,you might want to stop right here and at least read through the exercises.
Before adding the Twitter search logic,there are a few more interesting topics to cover.
Formatting of Pipelines
When you’re delving into formatting ReactiveCocoa code,the generally accepted convention is to have each operation on a new line,and align all of the steps vertically.
In this next image,you can see the alignment of a more complex example,taken from the prevIoUs tutorial:
Unfortunately,Xcode doesn’t really like this style of formatting,so you might find yourself battling with its automatic indentation logic!
Memory Management
Considering the code you added to theTwitterInstantapp,are you wondering how the pipeline you just created is retained? Surely,as it is not assigned to a variable or property it will not have its reference count incremented and is doomed to destruction?
One of the design goals of ReactiveCocoa was to allow this style of programming,where pipelines can formanonymously. In all of the reactive code you’ve written so far,this should seem quite intuitive.
In order to support this model,ReactiveCocoa maintains and retains its own global set of signals. If it has one or more subscribers,then the signal is active. If all subscribers are removed,the signal can be de-allocated. For more information on how ReactiveCocoa manages this process see theMemory Managementdocumentation.
That leaves on final question: How do you unsubscribe from a signal? After acompletedorerrorevent,a subscription removes itself automatically (you’ll learn more about this shortly). Manual removal may be accomplished viaRACDisposable
.
The subscription methods onRACSignal
all return an instance ofRACDisposable
that allows you to manually remove the subscription via the dispose method. Here is a quick example using the current pipeline:
RACSignal *backgroundColorSignal =
[self.searchText.rac_textSignal
map:^NSString *text) {
self isValidSearchText:text] ?
[UIColor yellowColor];
}];
RACDisposable *subscription =
[backgroundColorSignal
subscribeNext:^(UIColor *color) {
self.searchText.backgroundColor = color;
}];
// at some point in the future ...
[subscription dispose];
It is unlikely you’ll find yourself doing this very often,but it is worth knowing the possibility exists.
Note:As a corollary to this,if you create a pipeline but do not subscribe to it,the pipeline never executes,this includes any side-effects such asdoNext:
blocks.
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:
[[subscribeNext:block usesself
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 betweenself
and this signal,it will result in a retain cycle. Whether this matters or not depends on the lifecycle of theself
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 forWorking With Blocksrecommends capturing a weak reference toself
. With the current code you can achieve this as follows:
__weak RWSearchFormViewController *bself = self; // Capture the weak reference
[[UIColor *color) {
bself.searchText.backgroundColor = color;
}];
In the above codebself
is a reference toself
that has been marked as__weak
in order to make it a weak reference. Notice that thesubscribeNext:
block now uses thebself
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)
[[UIColor *color) {
@strongify(self)
@weakifyand@strongify
statements above are macros defined in theExtended Objective-Clibrary,and they are also included in ReactiveCocoa. The@weakify
macro allows you to createshadowvariables 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
.
Note:If you’re interested in finding out what
@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 toself
. You can turn on a compiler warning to alert you if your code results in this problem. Search forretainwithin the project’s build settings to find the options indicated below:
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 thesubscribeNext:
block in the current pipeline by making use of theRAC
macro. If you spotted this,make that change and award yourself a shiny gold star!
Requesting Access to Twitter
You’re going to use theSocial Frameworkin order to allow the TwitterInstant application to search for Tweets,and theAccounts Frameworkin order to grant access to Twitter. For a more detailed overview of theSocial Framework,check out the chapter dedicated to this framework iniOS 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 theSettingsapp and select theTwittermenu option,then add your credentials on the right hand side of the screen:
#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;
nonatomic) ACAccountType *twitterAccountType;
ACAccountsStoreclass provides access to the various social media accounts your device can connect to,and theACAccountType
class represents a specific type of account.
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!
// 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,anextevent followed by acompletedare sent. If the user denies access,anerrorevent 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 morenextevents followed by either acompletedevent or anerrorevent.
Finally,in order to make use of this signal,164)">self
requestAccessToTwitterSignal]
subscribeNext:^(id x) {
NSLog(@"Access granted");
} error:^(NSError *error) {
@"An error occurred: %@",error);
}];
If you build and run,the following prompt should greet you::
The Accounts Framework remembers the decision you made. Therefore to test bothpathsyou need to reset the simulator via theiOS Simulator -> Reset Contents and Settings …menu option. This is a bit of a pain because you also have to re-enter your Twitter credentials!
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 ofviewDidLoad
with the following:
[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
@strongify(return self.searchText.rac_textSignal;
}]
subscribeNext:^(@"%@",x);
} error:^(thenmethod waits until acompletedevent 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 weakifiedself
for the pipeline that sits just above this one,so there is no need to precede this pipeline with a@weakify(self)
.
thenmethod passeserrorevents through. Therefore the finalsubscribeNext: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 afilter
operation to the pipeline to remove any invalid search strings. In this instance,they are strings comprised of less than three characters:
[[[[self.searchText.rac_textSignal;
}]
filter:^BOOL(NSString *text) {
@strongify(self isValidSearchText:text];
}]
subscribeNext:^( 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:
events pass through a filter and finally onto the subscription block. You can also see anyerrorevents emitted by the first step are consumed by the samesubscribeNext: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
TheSocial Frameworkis an option to access the Twitter Search API. However,as you might expect,theSocial Frameworkis not reactive! The next step is towrapthe required API method calls in a signal. You should be getting the hang of this process by now!
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 thev1.1 REST API. The above code uses theq
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 theTwitter 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:// 2 - create the signal block
@weakify(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,1)">NSHTTPURLResponse *urlResponse,1)">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];
}
}];
}
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 anextevent,followed by acompletedevent.
- In the event of an unsuccessful response,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline"> Now to put this new signal to use!
In the first part of this tutorial you learnt how to useflattenMap
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 ofviewDidLoad
update your application pipeline by adding aflattenMap
step at the end:
[[[[[self isValidSearchText:text];
}]
flattenMap:^RACStream *(self signalForSearchWithText:text];
}]
subscribeNext:^( 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" = "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 = ...
signalForSearchText:method also emitserrorevents which thesubscribeNext:error:
block consumes. Youcouldtake my word for this,but you’d probably like to test it out!
Within the simulator open up theSettingsapp and select yourTwitteraccount,then delete it by tapping theDelete Accountbutton:
2014-01-05 52:11.705 TwitterInstant[41374:1403] An error occurred: Error
Domain=TwitterInstant Code=1 "The operation couldn’t be completed. (TwitterInstant error 1.)"
Code=1indicates this is theRWTwitterInstantErrorNoTwitterAccounts
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 abouterrorevents; 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!
Threading
I’m sure you’re itching to wire-up the JSON output of the Twitter search to the UI,but before you do that there is one last thing you need to do. To find out what this is,you need to do a bit of exploration!
Add a breakpoint to thesubscribeNext:error:
step at the location indicated below:
This illustrates an important point about the ReactiveCocoa framework. The operations shown above execute on the thread where the signal originally emitted its events. Try adding breakpoints at the other pipeline steps,you might be surprised to find they execute on more than one different thread!
So how do you go about updating the UI? The typical approach is to use operation queues (see the tutorialHow To Use NSOperations and NSOperationQueueselsewhere on this site for more details),however ReactiveCocoa has a much simpler solution to this problem.
Update your pipeline by adding adeliverOn:
operation just afterflattenMap:
as shown below:
[[[[[[self signalForSearchWithText:text];
}]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^( Now re-run the app and type some text so your app hits the breakpoint. You should see the log statement in yoursubscribeNext:error:
block is now executing on the main thread:
You can safely proceed to update your UI!
NOTE:If you take a look at theRACScheduler
class you’ll see that there is quite a range of options for delivering on threads with different priorities,or adding delays into pipelines.
It’s time to see those tweets!
Updating the UI
If you openRWSearchResultsViewController.h
you’ll see it already has adisplayTweets:
method,which will cause the right-hand view controller to render the supplied array of tweets. The implementation is very simple,it’s just a standardUITableView
datasource. The single argument for thedisplayTweets:
method expects anNSArray
containingRWTweet
instances. You’ll also find theRWTweet
model object was provided as part of the starter project.
The data which arrives at thesubscibeNext:error:
step is currently anNSDictionary
,which was constructed by parsing the JSON response insignalForSearchWithText:
. So how do you determine the contents of this dictionary?
If you take a look at theTwitter API documentationyou can see a sample response. TheNSDictionary
mirrors this structure,so you should find that it has a key namedstatuses
that is aNSArray
of tweets,which are alsoNSDictionary
instances.
If you look atRWTweet
it already has a class methodtweetWithStatus:
which takes anNSDictionary
in the given format and extracts the required data. So all you need to do is write a for loop,and iterate over the array,creating an instance ofRWTweet
for each tweet.
However,you’re not going to do that! Oh no,there’s much better things in store!
This article is about ReactiveCocoa and Functional Programming. The transformation of data from one format to another is more elegant when you use a functional API. You’re going to perform this task withLinqToObjectiveC.
Close the TwitterInstant workspace,and then open the Podfile that you created in the first tutorial,in TextEdit. Update the file to add the new dependency:
platform :ios,'7.0'
pod 'ReactiveCocoa',79)">'2.1.8'
pod 'LinqToObjectiveC',79)">'2.0.0'
Open up a terminal window in the same folder and issue the following command:
pod update
You should see output similar to the following:
Analyzing dependencies
Downloading dependencies
Installing LinqToObjectiveC (2.0.0)
Using ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project
Re-open the workspace and verify the new pod is showing as shown in the image below:
OpenRWSearchFormViewController.mand add the following imports to the top of the file:
"RWTweet.h"
"NSArray+LinqExtensions.h"
NSArray+LinqExtensions.hheader is fromLinqToObjectiveC,and adds a number of methods toNSArray
that allow you to transform,sort,group and filter its data using a fluent API.
Now to put this API to use … update the current pipeline at the end ofviewDidLoad
as follows:
[[[[[[self signalForSearchWithText:text];
}]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(NSDictionary *jsonSearchResult) {
NSArray *statuses = jsonSearchResult[@"statuses"];
NSArray *tweets = [statuses linq_select:^id(id tweet) {
return [RWTweet tweetWithStatus:tweet];
}];
[self.resultsViewController displayTweets:tweets];
} error:^( As you can see above,55)">subscribeNext:block first obtains the NSArray of tweets. Thelinq_select
method transforms the array ofNSDictionary
instances by executing the supplied block on each array element,resulting in an array ofRWTweet
instances.
Once transformed,the tweets get sent to the results view controller.
Build and run to finally see the tweets appearing in the UI:
Note:ReactiveCocoa and LinqToObjectiveC have similar sources of inspiration. Whilst ReactiveCocoa was modelled on Microsoft’sReactive Extensionslibrary,LinqToObjectiveC was modelled on their Language Integrated Query APIs,or LINQ,specificallyLinq to Objects.
Asynchronous Loading of Images
You’ve probably noticed there is a gap to the left of each tweet. That space is there to show the Twitter user’s avatar.
RWTweetclass already has aprofileImageUrl
property that is populated with a suitable URL for fetching this image. In order for the table view to scroll smoothly,you need to ensure the code that fetches this image from the given URL is not executed on the main thread. This can be achieved using Grand Central Dispatch or NSOperationQueue. But why not use ReactiveCocoa?
RWSearchResultsViewController.m
and add the following method to the end of the file:
-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl {
RACScheduler *scheduler = [RACScheduler
schedulerWithPriority:RACSchedulerPriorityBackground];
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
UIImage *image = [UIImage imageWithData:data];
[subscriber sendNext:image];
[subscriber sendCompleted];
nil;
}] subscribeOn:scheduler];
}
You should be pretty familiar with this pattern by now!
The above method first obtains a background scheduler as you want this signal to execute on a thread other than the main one. Next,it creates a signal that downloads the image data and creates aUIImage
when it has a subscriber. The final piece of magic issubscribeOn:
,which ensures that the signal executes on the given scheduler.
Magic!
Now,within the same file update thetableView:cellForRowAtIndex:
method by adding the following just before thereturn
statement:
cell.twitterAvatarView.image = nil;
[[[self signalForLoadingImage:tweet.profileImageUrl]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(UIImage *image) {
cell.twitterAvatarView.image = image;
}];
The above first resets the image since these cells are reused and could therefore containstaledata. Then it creates the required signal to fetch the image data. ThedeliverOn:
pipeline step,you encountered prevIoUsly,marshals thenextevent onto the main thread so that thesubscribeNext:
block can be safely executed.
Nice and simple!
Build and run to see that the avatars now display correctly:
Throttling
You might have noticed that every time you type a new character,a Twitter search executes immediately. If you’re a fast typer (or simply hold down the delete key),this can result in the application performing several searches a second. This is not ideal for a couple of reasons: firstly,you’rehammeringthe Twitter search API and simultaneously throwing away most of the results. Secondly,you’re constantly updating the results which is rather distracting for the user!
A better approach would be to perform a search only if the search text is unchanged for a short amount of time,say 500 milliseconds.
As you’ve probably guessed,ReactiveCocoa makes this task incredibly simple!
RWSearchFormViewController.mand update the pipeline at the end ofviewDidLoad
by adding a throttle step just after the filter:
[[[[[[[self isValidSearchText:text];
}]
throttle:0.5]
flattenMap:^RACStream *(throttleoperation will only send anextevent if anothernextevent isn’t received within the given time period. It’s really that simple!
Build and run to confirm that the search results only update if you stop typing for 500 milliseconds. Feels much better doesn’t it? Your users will think so too.
And…with that final step your Twitter Instant application is complete. Give yourself a pat on the back and do a happy dance.
If you got lost somewhere in the tutorial you can download thefinal project(Don’t forget to runpod install
from the project’s directory before opening),or you can obtain the code fromGitHubwhere there is a commit for each Build & Run step in this tutorial.
Wrap Up
Before heading off and treating yourself to a victory cup of coffee,it’s worth admiring the final application pipeline:
Now you know that ReactiveCocoa is really quite awesome!
One final point,ReactiveCocoa makes it possible to use the Model View ViewModel,or theMVVM design pattern,which provides better separation of application logic and view logic. If anyone is interested in a follow-up article on MVVM with ReactiveCocoa,please let me know in the comments. I’d love to hear your thoughts and experiences!