iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (91 page)

Read iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) Online

Authors: Aaron Hillegass,Joe Conway

Tags: #COM051370, #Big Nerd Ranch Guides, #iPhone / iPad Programming

BOOK: iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
9.17Mb size Format: txt, pdf, ePub
Using the Store

Before you implement the details of the store, you are going to implement the code in the controller that uses
fetchRSSFeedWithCompletion:
.
ListViewController
only needs to initiate the fetch and describe what will happen in the completion block it passes as the argument.

 

Right now, the
ListViewController
is responsible for creating the
NSURLRequest
and the
NSURLConnection
. It also implements the delegate methods for
NSURLConnection
that accumulate the response data from the server and execute code when the request either succeeds or fails. This code needs to be moved into the store; it is request logic. You can begin by deleting the body of
fetchEntries
in
ListViewController.m
.

 
- (void)fetchEntries
{
    
xmlData = [[NSMutableData alloc] init];
    
NSURL *url = [NSURL URLWithString:@"http://forums.bignerdranch.com"
                  
@"/smartfeed.php?@"limit=1_DAY&sort_by=standard"
                  
@"&feed_type=RSS2.0&feed_style=COMPACT"];
    
NSURLRequest *req = [NSURLRequest requestWithURL:url];
    
connection = [[NSURLConnection alloc] initWithRequest:req
                                                
delegate:self
                                        
startImmediately:YES];
}
 

Now let’s re-implement
fetchEntries
to leverage the store’s power. At the top of
ListViewController.m
, import the header for
BNRFeedStore
.

 
#import "BNRFeedStore.h"

Enter the new code for
fetchEntries
in
ListViewController.m
.

 
- (void)fetchEntries
{
    // Initiate the request...
    [[BNRFeedStore sharedStore] fetchRSSFeedWithCompletion:
    ^(RSSChannel *obj, NSError *err) {
        // When the request completes, this block will be called.
        if (!err) {
            // If everything went ok, grab the channel object, and
            // reload the table.
            channel = obj;
            [[self tableView] reloadData];
        } else {
            // If things went bad, show an alert view
            UIAlertView *av = [[UIAlertView alloc]
                                        initWithTitle:@"Error"
                                              message:[err localizedDescription]
                                             delegate:nil
                                    cancelButtonTitle:@"OK"
                                    otherButtonTitles:nil];
            [av show];
        }
    }];
}
 

Now, we won’t be able to build and run for some time, but let’s see what will happen (
Figure 28.6
).
ListViewController
asks
BNRFeedStore
for the RSS feed and says,

When you have the feed, do the stuff in this block.

The
ListViewController
moves on with its life, and the store gets to work.

 

Figure 28.6  Flow of Nerdfeed with BNRFeedStore

 

Look at the code in the block that gets executed if there is no
err
; it looks a lot like the code in
ListViewController
’s
connectionDidFinishLoading:
method. The code in the block that gets executed if there is an error looks just like the code in
connection:didFailWithError:
.

 

This, of course, is intentional. We are not changing what
Nerdfeed
does – only how it does it.
ListViewController
will no longer manage the details of the
NSURLConnection
. In MVCS, controllers don’t do that kind of work, store objects do.

 

ListViewController
now flows exceptionally well: it says,

Fetch these entries, and then do this if this happens or that if that happens.

All of this code is in one place instead of strewn over a number of methods. The code is easy to read, easy to maintain, and easy to debug.

 

With this code in place, you can clean up
ListViewController
. In
ListViewController.m
, you can delete the following methods. (The bodies of the methods have been omitted to save trees.)

 
- (void)parser:(NSXMLParser *)parser
    
didStartElement:(NSString *)elementName
      
namespaceURI:(NSString *)namespaceURI
      
qualifiedName:(NSString *)qualifiedName
        
attributes:(NSDictionary *)attributeDict
{
        
...
}
- (void)connection:(NSURLConnection *)conn didReceiveData:(NSData *)data
{
    
[xmlData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)conn
{
    
...
}
- (void)connection:(NSURLConnection *)conn
  
didFailWithError:(NSError *)error
{
    
...
}
 

In
ListViewController.h
, you can delete the instance variables
connection
and
xmlData
, as well as remove the protocol declaration for
NSXMLParserDelegate
.

 
@interface ListViewController : UITableViewController

{
    
NSURLConnection *connection;
    
NSMutableData *xmlData;
    RSSChannel *channel;
}
 

Take a moment to scroll through
ListViewController.h
and
ListViewController.m
. These files are now very sleek and easy to follow.
ListViewController
has a primary focus now – presenting a table of
RSSItem
s. This is the purpose of store objects: let the controller focus on its job rather than the details of request logic.

 

Another thing to mention – in the declaration of
fetchRSSFeedWithCompletion:
in
BNRFeedStore.h
, you supplied the names of the arguments for the block argument. In
Chapter 27
, we mentioned that this wasn’t necessary because the names of the arguments only matter in the block literal.

 

However, when you were typing
fetchRSSFeedWithCompletion:
in
ListViewController.m
, you probably noticed that
Xcode
auto-completed the block literal for you and added in the names of the arguments for the block. This was pretty handy. So when declaring a block variable as a property or as an argument to a method, it’s good idea to specify the names of the arguments in the block to take advantage of auto-completion.

 

You can build the application to check for syntax errors, but running it won’t do you much good since
fetchRSSFeedWithCompletion:
hasn’t been implemented. In the next section, you will complete the store object and get
Nerdfeed
back up and running.

 
Building BNRFeedStore

The code that was removed from
ListViewController
must be replaced in
BNRFeedStore
. The
BNRFeedStore
must handle preparing the
NSURLRequest
, kicking off the
NSURLConnection
, handling the response from the server, and starting the parser. The
ListViewController
simply asks for these actions to be initiated and supplies a callback for when they are completed.

 
Initiating the connection

A store’s job is to take a really simple request from a controller, like

fetch the RSS feed,

and prepare a detailed request tailored for the external source it interacts with. In this case, the detailed request is the web service call to
http://forums.bignerdranch.com
. In
BNRFeedStore.m
, begin implementing
fetchRSSFeedWithCompletion:
by preparing this request.

 
- (void)fetchRSSFeedWithCompletion:(void (^)(RSSChannel *obj, NSError *err))block
{
    NSURL *url = [NSURL URLWithString:@"http://forums.bignerdranch.com/"
                  @"smartfeed.php?limit=1_DAY&sort_by=standard"
                  @"&feed_type=RSS2.0&feed_style=COMPACT"];
    NSURLRequest *req = [NSURLRequest requestWithURL:url];
}
 

It is important that the
NSURLRequest
is created inside the store and not in
ListViewController
. It is not
ListViewController
’s job to understand how to interact with the web server; thus, it shouldn’t know about the URL or the arguments passed in that URL. In fact, the
ListViewController
shouldn’t even know that the Internet exists or what an
NSURL
is. It just wants an
RSSChannel
, and by golly, it’ll get one.

 

The store’s next step is to create an
NSURLConnection
to process this
NSURLRequest
. Your first intuition would be to create the
NSURLConnection
here in
fetchRSSFeedWithCompletion:
and set the delegate of the connection as the
BNRFeedStore
. However, what happens when we add more requests to the store?

 

For example, the store could pull the RSS feed from another server. The store, then, would have to be the delegate for a lot of
NSURLConnection
s. The
NSURLConnection
delegate methods that handle the response would become giant if-else statements to determine what data belongs to which request.

 
The actor design pattern

Instead of having the
BNRFeedStore
be the delegate for every
NSURLConnection
, we are going to create another class named
BNRConnection
. The sole purpose of an instance of this class will be to manage a single
NSURLConnection
.

 

BNRConnection
instances will take the request prepared by the
BNRFeedStore
, start their own
NSURLConnection
, and then receive all of the delegate messages from that
NSURLConnection
. This object will do its work without bothering the controller or the store until it has finished. When it finishes, it will execute the completion block supplied by
ListViewController
(
Figure 28.7
).

 

Figure 28.7  BNRConnection and NSURLConnection

 

This is an example of the
actor design pattern
. An actor object is used when you have a long running task and some code that needs to be executed after it completes. This kind of object is given the information it needs to perform the task and callbacks to execute when that task is done. The actor runs on its own thread without any further input and is destroyed when it is finished (
Figure 28.8
).

 

Figure 28.8  Actor design pattern

 

Create
BNRConnection
, the actor, as a new
NSObject
subclass.
BNRConnection
needs instance variables and properties to hold the connection, request, and callback. Modify
BNRConnection.h
to look like this:

 
@interface BNRConnection : NSObject
    

{
    NSURLConnection *internalConnection;
    NSMutableData *container;
}
- (id)initWithRequest:(NSURLRequest *)req;
@property (nonatomic, copy) NSURLRequest *request;
@property (nonatomic, copy) void (^completionBlock)(id obj, NSError *err);
@property (nonatomic, strong) id xmlRootObject;
- (void)start;
@end
 

For every request the
BNRFeedStore
makes, it will create an instance of
BNRConnection
. An instance of this class is a one-shot deal: it runs, it calls back, and then it is destroyed. The next time a request is made, another instance of
BNRConnection
will be created. The store’s responsibility here is to pass on the appropriate data (the request and callback block) to the
BNRConnection
so that it can function. In other words, a store object becomes a middle man: it takes the simple demands of a controller and sends its
BNRConnection
minions off to carry out the work.

 

In addition to the request and callback, the store will provide an
xmlRootObject
to the
BNRConnection
. This object will be the object that eventually gets returned to the controller. In this case, the
xmlRootObject
will be an instance of
RSSChannel
. The store will create an empty
RSSChannel
and give it to the
BNRConnection
. When the connection finishes, the
BNRConnection
will parse the data from the
NSURLConnection
into the
xmlRootObject
before handing it to the controller.

 

Before we write the code for
BNRConnection
, we will finish
fetchRSSFeedWithCompletion:
in
BNRFeedStore.m
. First, import the appropriate files into
BNRFeedStore.m
.

 
#import "RSSChannel.h"
#import "BNRConnection.h"

In
BNRFeedStore.m
, add code to
fetchRSSFeedWithCompletion:
that creates an instance of
BNRConnection
and sets it up with the request, completion block and empty root object.

 
- (void)fetchRSSFeedWithCompletion:(void (^)(RSSChannel *obj, NSError *err))block
{
    NSURL *url = [NSURL URLWithString:@"http://forums.bignerdranch.com/"
                  @"smartfeed.php?limit=1_DAY&sort_by=standard"
                  @"&feed_type=RSS2.0&feed_style=COMPACT"];
    NSURLRequest *req = [NSURLRequest requestWithURL:url];
    
    // Create an empty channel
    RSSChannel *channel = [[RSSChannel alloc] init];
    // Create a connection "actor" object that will transfer data from the server
    BNRConnection *connection = [[BNRConnection alloc] initWithRequest:req];
    // When the connection completes, this block from the controller will be called
    [connection setCompletionBlock:block];
    // Let the empty channel parse the returning data from the web service
    [connection setXmlRootObject:channel];
    // Begin the connection
    [connection start];
}
 

We are now at the heart of the code that will transform
Nerdfeed
from a pedagogical example to a functional powerhouse: the code for
BNRConnection
. We’ve changed the code in
Nerdfeed
by working from the outside and moving in, allowing us to see the simplicity earned by using MVCS and then the work required for that simplicity. We are almost there.

 

An instance of
BNRConnection
, when started, will create an instance of
NSURLConnection
, initialize it with an
NSURLRequest
, and set itself as the delegate of that connection. In
BNRConnection.m
, implement the following two methods and synthesize the three properties.

 
@implementation BNRConnection
@synthesize request, completionBlock, xmlRootObject;
- (id)initWithRequest:(NSURLRequest *)req
{
    self = [super init];
    if (self) {
        [self setRequest:req];
    }
    return self;
}
- (void)start
{
    // Initialize container for data collected from NSURLConnection
    container = [[NSMutableData alloc] init];
    // Spawn connection
    internalConnection = [[NSURLConnection alloc] initWithRequest:[self request]
                                                         delegate:self
                                                 startImmediately:YES];
}

Build the application and check for syntax errors.

 

There is one little hitch here. When the store creates an instance of
BNRConnection
and tells it to start, the store will no longer keep a reference to it. This technique is known as

fire and forget.

However, with ARC, we know that

forgetting

about an object will destroy it. Thus, the
BNRConnection
needs to be owned by something. At the top of
BNRConnection.m
, add a static
NSMutableArray
variable to hold a strong reference to all active
BNRConnection
s.

 
#import "BNRConnection.h"
static NSMutableArray *sharedConnectionList = nil;
@implementation BNRConnection
@synthesize request, completionBlock, xmlRootObject;

(As a bonus, you can use this connection list as a way to reference connections in progress. This is especially useful for cancelling a request.)

 

In
BNRConnection.m
, update
start
to add the connection to this array so that it doesn’t get destroyed when the store forgets about it.

 
- (void)start
{
    container = [[NSMutableData alloc] init];
    internalConnection = [[NSURLConnection alloc] initWithRequest:[self request]
                                                         delegate:self
                                                 startImmediately:YES];
    
    // If this is the first connection started, create the array
    if (!sharedConnectionList)
        sharedConnectionList = [[NSMutableArray alloc] init];
    // Add the connection to the array so it doesn't get destroyed
    [sharedConnectionList addObject:self];
}
 

Other books

Opening Moves by James Traynor
Stone Age by ML Banner
Complete Abandon by Julia Kent
Risking it All by Tessa Bailey
Two Short Novels by Mulk Raj Anand
The Winter's Tale by William Shakespeare