ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2


原文地址: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 aUITableViewControllersubclass.

If you open upRWSearchFormViewController.myou can see theviewDidLoadmethod locates the results view controller and assigns it to theresultsViewControllerprivate 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’sbackgroundColorproperty 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:

Therac_textSignalemitsnextevents containing the current text field’s text each time a change occurs. The map step transforms the text value into a color,while thesubscribeNext:step takes this value and applies it to the text field background.

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:

This allows you to see the operations that make up the pipeline very easily. Also,minimize the amount of code in each block; anything more than a couple of lines should be broken out into a private method.

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 onRACSignalall return an instance ofRACdisposablethat 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 usesselfin order to obtain a reference to the text field. Blocks capture and retain values from the enclosing scope,therefore if a strong reference exists betweenselfand this signal,it will result in a retain cycle. Whether this matters or not depends on the lifecycle of theselfobject. 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 codebselfis a reference toselfthat has been marked as__weakin order to make it a weak reference. Notice that thesubscribeNext:block Now uses thebselfvariable. 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@strongifystatements above are macros defined in theExtended Objective-Clibrary,and they are also included in ReactiveCocoa. The@weakifymacro allows you to createshadowvariables which are weak references (you can pass multiple variables if you require multiple weak references),the@strongifymacro allows you to create strong references to variables that were prevIoUsly passed to@weakify.

Note:If you’re interested in finding out what @weakifyand @strongifyactually 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:

Okay,you survived the theory,congrats! Now you’re much wiser and ready to move on to the fun part: adding somerealfunctionality 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 thesubscribeNext:block in the current pipeline by making use of theRACmacro. 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:

The starter project already has the required frameworks added,so you just need to import the headers. WithinRWSearchFormViewController.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;
nonatomic) ACAccountType *twitterAccountType;

ACAccountsStoreclass provides access to the various social media accounts your device can connect to,and theACAccountTypeclass 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:

  1. An error is defined,which is sent if the user refuses access.
  2. As per the first article,the class methodcreateSignalreturns an instance ofRACSignal.
  3. 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.
  4. 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::

If you tapOK,the log message in thesubscribeNext:block should appear in the console,whereas,if you tapDon’t Allow,the error block executes and logs the respective message.

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 ofviewDidLoadwith 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 weakifiedselffor 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 afilteroperation 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:

The application pipeline starts with therequestAccessToTwitterSignalthen switches to therac_textSignal. Meanwhile,55)">nextevents 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 theqsearch 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:

  1. 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.
  2. As before,a signal is created.
  3. Create a request for the given search string using the method you added in the prevIoUs step.
  4. Query the account store to find the first available Twitter account. If no accounts are given,an error is emitted.
  5. The request executes.
  6. 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.
  7. 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 useflattenMapto 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 ofviewDidLoadupdate your application pipeline by adding aflattenMapstep 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:

    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 thesignalForSearchTextmethod will emit an error,which will be logged:

    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 theRWTwitterInstantErrorNoTwitterAccountserror. 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:

    Re-run the application,re-enter your Twitter credentials again if needed,and type some text into the search field. When the breakpoint hits you should see something similar to the image below:

    Notice the code where the debugger hit a break is not executed on the main thread,which appears asThread 1in the above screenshot. Keep in mind that it’s paramount you only update the UI from the main thread; therefore if you want to display the list of tweets in the UI you’re going to have to switch threads.

    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:

    What? There’s just one simple operation for marshalling the flow of events onto a different thread? Just how awesome is that!?

    You can safely proceed to update your UI!

    NOTE:If you take a look at theRACSchedulerclass 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.hyou’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 standardUITableViewdatasource. The single argument for thedisplayTweets:method expects anNSArraycontainingRWTweetinstances. You’ll also find theRWTweetmodel 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. TheNSDictionarymirrors this structure,so you should find that it has a key namedstatusesthat is aNSArrayof tweets,which are alsoNSDictionaryinstances.

    If you look atRWTweetit already has a class methodtweetWithStatus:which takes anNSDictionaryin 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 ofRWTweetfor 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 toNSArraythat 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 ofviewDidLoadas 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_selectmethod transforms the array ofNSDictionaryinstances by executing the supplied block on each array element,resulting in an array ofRWTweetinstances.

    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 aprofileImageUrlproperty 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.mand 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 aUIImagewhen 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 thereturnstatement:

    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 ofviewDidLoadby 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 installfrom 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

    Want to learn even faster? Save time with ourvideo courses

    Before heading off and treating yourself to a victory cup of coffee,it’s worth admiring the final application pipeline:

    That’s quite a complicated data flow,all expressed concisely as a single reactive pipeline. It’s a beautiful sight to see! Can you imagine how much more complex this application would be using non-reactive techniques? And how much harder it would be to see the data flows in such an application? Sounds very cumbersome,and now you don’t have to go down that road ever again!

    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!

相关文章

一、前言 在组件方面react和Vue一样的,核心思想玩的就是组件...
前言: 前段时间学习完react后,刚好就接到公司一个react项目...
前言: 最近收到组长通知我们项目组后面新开的项目准备统一技...
react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom...