iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (94 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)
12.5Mb size Format: txt, pdf, ePub
 

A model object, then, provides the instructions on how it is to be transferred into and out of different formats. Model objects do this so that when a store object transfers them to or from an external source, the store doesn’t need to know the details of every kind of model object in an application. Instead, each model object can conform to a protocol that says,

Here are the formats I know how to be in,

and the store triggers the transfer into or out of those data formats.

 
More on Store Objects

Let’s recap what we know about stores so far. A store object handles the details of interfacing with an external source. The complicated request logic required to interact with these sources becomes the responsibility of the store so that the controller can focus on the flow of the application. We’ve really seen two types of stores: synchronous and asynchronous.
BNRItemStore
and
BNRImageStore
were examples of synchronous stores: their work could be finished immediately, and they could return a value from a request right away.

 

CLLocationManager
and
BNRFeedStore
are examples of asynchronous stores: their work takes awhile to finish. With asynchronous stores, you have to supply callbacks for the controller to receive a response. Asynchronous stores are a bit more difficult to write, but we’ve found ways of reducing that difficulty, such as using helpers objects like
BNRConnection
.

 

The cool thing about asynchronous stores is that even though a lot of objects are doing a lot of work to process a request, the flow of the request and its response are in one place in the controller. This gives us the benefit of code that’s easy to read and also easy to modify. As an example, let’s add a
UIActivityIndicatorView
to the
ListViewController
when a request is being made. In
ListViewController.m
, update
fetchEntries
.

 
- (void)fetchEntries
{
    // Get ahold of the segmented control that is currently in the title view
    UIView *currentTitleView = [[self navigationItem] titleView];
    // Create a activity indicator and start it spinning in the nav bar
    UIActivityIndicatorView *aiView = [[UIActivityIndicatorView alloc]
                    initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
    [[self navigationItem] setTitleView:aiView];
    [aiView startAnimating];
    void (^completionBlock)(RSSChannel *obj, NSError *err) =
    ^(RSSChannel *obj, NSError *err) {
        
        // When the request completes - success or failure - replace the activity
        // indicator with the segmented control
        [[self navigationItem] setTitleView:currentTitleView];
        
        if (!err) {
 

Build and run the application. When you change between feeds, you will see the activity indicator spin briefly while the request is processing. Also, notice how the block takes ownership of the
UISegmentedControl
that was the
titleView
of the navigation item so that it can put it back into the navigation item once the request has completed. Pretty neat, huh?

 

We’ve done a lot of work so far in this chapter. It may seem like overkill since we already had a working application, but as the application becomes larger in the next two chapters, the changes we’ve made will really pay off. Consider already how easy it was to add support for JSON data – we didn’t even touch
ListViewController
! In the next chapter, we’ll do a bit more work with
BNRFeedStore
and then explain some best practices and guidelines for using store objects.

 
Bronze Challenge: UI for Song Count

Add an interface element somewhere that allows the user to change the number of songs pulled from the top songs service.

 
Mega-Gold Challenge: Another Web Service

We provide our class schedule in JSON format at
http://www.bignerdranch.com/json/schedule
. Create an entirely new application to present the class schedule and details about each class using MVCS.

 
For the More Curious: JSON Data

JSON is a data interchange format – a way of describing data so it can be transferred to another system easily. It has a really concise and easy to understand syntax. First, let’s think about why we need to transfer data. An application has some model objects that represent things – like items in an RSS feed. It would make sense that this application would want to share these items with another device.

 

However, let’s say that other device is an Android phone (and, in our imaginary world, let’s pretend this Android phone’s battery is not drained dry). We can’t just package up an Objective-C
RSSItem
object and send it to the Android device. We need an agreed-upon format that both systems understand. Both Java (the language Android is programmed in) and Objective-C have a concept of objects, and both platforms have the ideas of arrays, strings, and numbers. JSON reduces data down to this very pure format of objects, arrays, strings and numbers, so the two platforms can share data.

 

In JSON, there is a syntax for each of these types. Objects are represented by curly brackets. Inside an object can be a number of members, each of which is a string, number, array, or another object. Each member has a name. Here is an example of an object that has two members, one a string, the other a number.

 
{
    "name":"Joe Conway",
    "age":28
}
 

When this JSON object is parsed by
NSJSONSerialization
, it is turned into an
NSDictionary
. That dictionary has two key-value pairs:
name
which is
Joe Conway
and
age
which is
28
. On another platform, this object would be represented in some other way, but on iOS, it is a dictionary because that is what makes the most sense. Additionally, all keys within the dictionary will be instances of
NSString
. The string value is also an instance of
NSString
, and the number value is an instance of
NSNumber
.

 

An array is described by using square brackets in JSON. It can contain objects, strings, numbers, and more arrays. Here is an example of an array that contains two objects:

 
[
    {
        "name":"Joe Conway",
        "age":28
    },
    {
        "name":"Aaron Hillegass",
        "age":87
    }
]

When this JSON is parsed, you will get an
NSArray
containing two
NSDictionary
instances. Each
NSDictionary
will have two key-value pairs. (One of those key-value pairs is going to get me fired...)

 

JSON data is really good at describing a tree of objects – that is, an object that has references to more objects, and each of those have references to more objects. This means when you deserialize JSON data, you are left with a lot of interconnected objects. (We call these interconnected objects an
object graph
.) The only real requirement of JSON – other than staying within the syntax – is that the top-level entry be either an object or an array. Thus, you will only ever get back an
NSDictionary
or
NSArray
from
NSJSONSerialization
.

 
29
Advanced MVCS

In this chapter, we’re going to add to
Nerdfeed
by adding support for caching and a way to indicate to users which posts have already been read.

 
Caching the RSS Feed

Each time the user fetches the Apple top songs RSS feed, the
BNRFeedStore
makes a web service request to Apple’s server. However, it takes time and battery power to make these requests. The data in this feed doesn’t change very often, either. Therefore, it doesn’t make much sense to make the resource-consuming request over and over again.

 

Instead, the store should save the results of the last request made. If the user tries to reload the feed within five minutes, the store won’t bother making the request and will just return the saved results. This process is known as
caching
.

 

Generally speaking, a cache is data that has been saved in another place where it is easier to retrieve. For example, it is easier to retrieve data from the local filesystem than from a server somewhere out in Internet land. In
Nerdfeed
, the cache will be the
RSSChannel
and the
RSSItem
s that represent the top ten songs. We will store this cache on the filesystem, so
RSSChannel
and
RSSItem
must be able to be saved and loaded.

 

For saving and loading these objects, it makes sense to use archiving instead of Core Data for three reasons. First, we won’t have that many records to save and load (just ten). Second, we don’t have a lot of relationships between objects (just a channel containing an array of items). Third, archiving is easier to implement.

 

Before you can write the code to save and load the cache,
RSSChannel
and
RSSItem
have to conform to
NSCoding
. In
RSSChannel.h
, declare that this class conforms to
NSCoding
.

 
@interface RSSChannel : NSObject
            , NSCoding
>
 

In
RSSChannel.m
, implement the two
NSCoding
methods.

 
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:items forKey:@"items"];
    [aCoder encodeObject:title forKey:@"title"];
    [aCoder encodeObject:infoString forKey:@"infoString"];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self) {
        items = [aDecoder decodeObjectForKey:@"items"];
        [self setInfoString:[aDecoder decodeObjectForKey:@"infoString"]];
        [self setTitle:[aDecoder decodeObjectForKey:@"title"]];
    }
    return self;
}
 

Do the same thing in
RSSItem.h
.

 
@interface RSSItem : NSObject
    , NSCoding
>
 

And in
RSSItem.m
.

 
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:title forKey:@"title"];
    [aCoder encodeObject:link forKey:@"link"];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self) {
        [self setTitle:[aDecoder decodeObjectForKey:@"title"]];
        [self setLink:[aDecoder decodeObjectForKey:@"link"]];
    }
    return self;
}
 

Now that channels and items can be written to the filesystem and loaded back into the application, let’s figure out when and how this should happen. Typically when you cache something, you use an all-or-nothing approach: a request either returns all brand-new data or all cached data. This approach makes sense for Apple’s Top Songs RSS feed because the amount of total data is small. Because the data isn’t updated very often, we will return cached data to the controller unless that data is more than five minutes old.

 

Caching is another task for the store rather than the controller. The controller doesn’t have to know the details or even whether it is getting cached or new data.
ListViewController
will ask for the RSS feed, and the
BNRFeedStore
will return the cached data if it is fresh. Otherwise, it will make the request to Apple’s server for fresh data (
Figure 29.1
).

 

Figure 29.1  Cache flow

 

For
BNRFeedStore
to make that choice, it needs to know when the
RSSChannel
was last cached. In
BNRFeedStore.h
, add a new property.

 
@property (nonatomic, strong) NSDate *topSongsCacheDate;
 

We need this date to persist between runs of the application, so it needs to be stored on the filesystem. Since this is just a little bit of data, we won’t create a separate file for it. Instead, we’ll just put it in
NSUserDefaults
.

 

To do this, you have to write your own implementations of
topSongsCacheDate
’s accessor methods instead of synthesizing the property. In
BNRFeedStore.m
, implement the accessor methods to access the preferences file.

 
- (void)setTopSongsCacheDate:(NSDate *)topSongsCacheDate
{
    [[NSUserDefaults standardUserDefaults] setObject:topSongsCacheDate
                                              forKey:@"topSongsCacheDate"];
}
- (NSDate *)topSongsCacheDate
{
    return [[NSUserDefaults standardUserDefaults]
                        objectForKey:@"topSongsCacheDate"];
}
 

Notice that we put this code in the store. This is because, once again, you are interacting with an external source (the preferences file on the filesystem). Additionally, this code is related to fetching the RSS feed.

 

From outside the store,
topSongsCacheDate
looks like a standard property – a set of methods that access an instance variable. That’s good. The store does all of this interesting stuff while making its job look easy. Stores are kind of like Olympic athletes – you don’t see the lifetime of hard work; you just see a few seconds of effortless speed.

 

For a store to be able to return the cached
RSSChannel
, it first needs to cache it. Every time the store fetches the top songs feed, we want it to save the channel and its items, as well as note the time they were cached. However, the way
BNRFeedStore
is set up right now, the only code that is executed when a request completes is the block supplied by
ListViewController
. This means the
BNRFeedStore
doesn’t know when the request finishes, so it doesn’t have a chance to cache the data. Thus, the store needs to add a callback to the
BNRConnection
that handles the request.

 

We already have a way to do this: the store can create its own completion block for the
BNRConnection
to execute. But alas, that spot is already reserved for the
ListViewController
’s completion block. Fortunately, you can execute a block inside another block. So we’re going to have the completion block for the store run its code (which will take care of the caching) and then execute the completion block from the controller. This one-two punch of blocks will become the
completionBlock
for the
BNRConnection
.

 

In
BNRFeedStore.m
, update
fetchTopSongs:withCompletion:
.

 
- (void)fetchTopSongs:(int)count
       withCompletion:(void (^)(RSSChannel *obj, NSError *err))block
{
    // Construct the cache path
    NSString *cachePath =
        [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
                                             NSUserDomainMask,
                                             YES) objectAtIndex:0];
    cachePath = [cachePath stringByAppendingPathComponent:@"apple.archive"];
    
    NSString *requestString = [NSString stringWithFormat:
                @"http://itunes.apple.com/us/rss/topsongs/limit=%d/json", count];
    NSURL *url = [NSURL URLWithString:requestString];
    NSURLRequest *req = [NSURLRequest requestWithURL:url];
    RSSChannel *channel = [[RSSChannel alloc] init];
    BNRConnection *connection = [[BNRConnection alloc] initWithRequest:req];
    
[connection setCompletionBlock:block];
    
    [connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
        // This is the store's completion code:
        // If everything went smoothly, save the channel to disk and set cache date
        if (!err) {
            [self setTopSongsCacheDate:[NSDate date]];
            [NSKeyedArchiver archiveRootObject:obj toFile:cachePath];
        }
        // This is the controller's completion code:
        block(obj, err);
    }];
    [connection setJsonRootObject:channel];
    [connection start];
}
 

You can build and run the application now. While
Nerdfeed
can’t read the cache yet, it is able to archive the channel and note the date after you select the
Apple
feed. You can verify this by running the application on the simulator and locating the application’s directory in the simulator’s
Application Support
directory. (If you forgot how, flip back to
Chapter 14
and
the section called “NSKeyedArchiver and NSKeyedUnarchiver”
.) You can check the preferences file in
Library/Preferences/
for the
topSongsCacheDate
key and look for the archive in
Library/Caches/
.

 

The store now needs to return the cached data if it is still fresh. It should do this without letting the controller know it is getting cached data – after all, the controller doesn’t really care; it just wants the top ten songs. In
BNRFeedStore.m
, update
fetchTopSongs:withCompletion:
to do this.

 
- (void)fetchTopSongs:(int)count
       withCompletion:(void (^)(RSSChannel *obj, NSError *err))block
{
    NSString *cachePath =
        [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
                                             NSUserDomainMask,
                                             YES) objectAtIndex:0];
    cachePath = [cachePath stringByAppendingPathComponent:@"apple.archive"];
    
    // Make sure we have cached at least once before by checking to see
    // if this date exists!
    NSDate *tscDate = [self topSongsCacheDate];
    if (tscDate) {
        // How old is the cache?
        NSTimeInterval cacheAge = [tscDate timeIntervalSinceNow];
        if (cacheAge > -300.0) {
            // If it is less than 300 seconds (5 minutes) old, return cache
            // in completion block
            NSLog(@"Reading cache!");
            RSSChannel *cachedChannel = [NSKeyedUnarchiver
                                            unarchiveObjectWithFile:cachePath];
            if (cachedChannel) {
                // Execute the controller's completion block to reload its table
                block(cachedChannel, nil);
                // Don't need to make the request, just get out of this method
                return;
            }
        }
    }
    NSString *requestString = [NSString stringWithFormat:
                @"http://itunes.apple.com/us/rss/topsongs/limit=%d/json", count];
 

Build and run the application. Select the
Apple
item on the segmented control and notice the console says it is reading from the cache. The list will appear immediately, and no request will be made – you won’t see the activity indicator at all when switching to the
Apple
feed. (You can check again in five minutes and see that the feed is indeed fetched again. Or delete the application and run it again.)

Other books

The Mysterious Heir by Edith Layton
Marlene by Florencia Bonelli
Forgotten Suns by Judith Tarr
Deathskull Bombshell by Bethny Ebert
Catching Dragos by Gail Koger
Essence of Desire by Jackson, Brenda