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

There is a slight issue here, although you can’t see it. When
ListViewController
makes this request, it expects some time to pass before the request is completed – this is why it supplies a callback block to be executed when the request is finished. When the store decides to return cached data, the callback is executed immediately. This can cause an issue if the
ListViewController
has more code in
fetchEntries
after it makes the request because it expects that code to be executed before the completion block.

 

Let’s put in a two log statements in
ListViewController.m
to confirm this behavior.

 
- (void)fetchEntries
{
    UIView *currentTitleView = [[self navigationItem] titleView];
    UIActivityIndicatorView *aiView = [[UIActivityIndicatorView alloc]
            initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
    [[self navigationItem] setTitleView:aiView];
    [aiView startAnimating];
    void (^completionBlock)(id obj, NSError *err) =
    ^(RSSChannel *obj, NSError *err) {
        
NSLog(@"Completion block called!");
        [[self navigationItem] setTitleView:currentTitleView];
        if (!err) {
            channel = obj;
            [[self tableView] reloadData];
        } else {
            NSString *errorString = [NSString stringWithFormat:@"Fetch failed: %@",
                                     [err localizedDescription]];
            UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error"
                                                         message:errorString
                                                        delegate:nil
                                               cancelButtonTitle:@"OK"
                                               otherButtonTitles:nil];
            [av show];
        }
    };
    if (rssType == ListViewControllerRSSTypeBNR)
        [[BNRFeedStore sharedStore] fetchRSSFeedWithCompletion:completionBlock];
    else if (rssType == ListViewControllerRSSTypeApple)
        [[BNRFeedStore sharedStore] fetchTopSongs:10
                                   withCompletion:completionBlock];
    NSLog(@"Executing code at the end of fetchEntries");
}

Build and run the application and tap on the
Apple
segment. Notice that
Completion block called!
shows up in the console before the log statement at the end of
fetchEntries
when the data is cached. If the cache is out of date and fetched again, the order of the statements is reversed. The store should really be consistent about when it executes the callback for the request. In
BNRFeedStore.m
, add the following code to
fetchTopSongs:withCompletion:
.

 
- (void)fetchTopSongs:(int)count
       withCompletion:(void (^)(RSSChannel *obj, NSError *err))block
{
    NSString *cachePath =
        [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
                                             NSUserDomainMask,
                                             YES) objectAtIndex:0];
    cachePath = [cachePath stringByAppendingPathComponent:@"apple.archive"];
    NSDate *tscDate = [self topSongsCacheDate];
    if (tscDate) {
        NSTimeInterval cacheAge = [tscDate timeIntervalSinceNow];
        if (cacheAge > -300.0) {
            NSLog(@"Reading cache!");
            RSSChannel *cachedChannel = [NSKeyedUnarchiver
                unarchiveObjectWithFile:cachePath];
            if (cachedChannel) {
                
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    block(cachedChannel, nil);
                
}];
                return;
            }
        }
    }

Now when you build and run the application, the callback is always executed after
fetchEntries
completes because the completion block is inserted as the next event in the run loop instead of being called immediately.

 
Advanced Caching

The caching mechanism for Apple’s top songs feed is pretty simple: each time a new feed is fetched, it completely replaces the old feed in the cache. This makes sense for this particular feed – we aren’t maintaining a history of top songs, just showing the current top ten.

 

The Big Nerd Ranch forum feed is a little different. Every time a new feed is fetched, you are getting the last day’s worth of posts. If we cached this feed in the same way as Apple’s feed, we wouldn’t be able to maintain a history. Also, the forum feed can be pretty large since we get a lot of posts in a day, so it takes a few moments to load the feed. It would be better for the user if they could see a list of cached posts as soon as they open the application or switch back to the
BNR
feed. When new posts become available, they would be added to the top of the list.

 

Figure 29.2  Cache and update flow

 

We can accomplish this task by implementing caching for the Big Nerd Ranch forum feed in a different way than we did for the Apple feed. For BNR’s feed, each time a controller requests that the store fetch new items, the store will immediately return the cache and then ask the server for more items. When the items come back from the server, the store will merge the new items into its cached
RSSChannel
. Then, the store will inform the controller that the new items have been fetched so that the interface can be updated. This interaction is shown in
Figure 29.2
.

 

To pull this off, we need to do three things:

 
  • An
    RSSChannel
    needs to be able to append new
    RSSItem
    s to its
    items
    array and, in doing so, make sure that there are no duplicates and that the items are ordered by date.
 
  • The method
    fetchRSSFeedWithCompletion:
    needs to return the cached channel immediately and an updated channel when the request completes.
 
  • The
    ListViewController
    needs to appropriately handle updating its views when it receives the cached channel immediately and the updated channel later.
 

Each of these steps requires a few small steps to achieve. We will start by teaching the
RSSChannel
to filter new items into its existing
items
array. First, declare a new method in
RSSChannel.h
.

 
- (void)addItemsFromChannel:(RSSChannel *)otherChannel;
 

When the store is asked to fetch the RSS feed, we want it to unarchive the cached channel from disk, return it, and ask the server for another
RSSChannel
instance that has the new items in it. When the request completes, the cached channel should be sent
addItemsFromChannel:
with the new channel as an argument. So we want this method to add items from the new channel to the existing channel. One little problem: because the RSS feed returns all items from the last 24 hours, if you refresh the feed more than once a day, you will get duplicates.

 

The channel, then, has to determine whether it already has an item before adding it to its list. To do this, instances of
RSSItem
have to be checked for equality. Every
NSObject
subclass implements a method named
isEqual:
for this purpose. In
RSSItem.m
, override this method to make two
RSSItem
s equivalent if they have the same
link
.

 
- (BOOL)isEqual:(id)object
{
    // Make sure we are comparing an RSSItem!
    if (![object isKindOfClass:[RSSItem class]])
        return NO;
    // Now only return YES if the links are equal.
    return [[self link] isEqual:[object link]];
}
 

In
RSSChannel.m
, begin implementing
addItemsFromChannel:
so that it adds all items from another channel if there are no duplicates.

 
- (void)addItemsFromChannel:(RSSChannel *)otherChannel
{
    for (RSSItem *i in [otherChannel items]) {
        // If self's items does not contain this item, add it
        if (![[self items] containsObject:i])
            [[self items] addObject:i];
    }
}
 

Even though you do not use
RSSItem
’s
isEqual:
method explicitly,
NSArray
’s
containsObject:
does. The
containsObject:
method sends
isEqual:
to every object within the array, and if one responds
YES
, then
containsObject:
also returns
YES
. By inverting the return value of
containsObject:
,
addItemsFromChannel:
will only add an item if it doesn’t already have it.

 

Next, the
RSSChannel
needs to preserve the order of its items. The order of items should be dictated by when the item was posted – the newer an item is, the higher it should appear on the list. The post date of an item is available in the RSS feed, but
RSSItem
doesn’t currently collect that data. Let’s change that. In
RSSItem.h
, declare a new property to hold the post date.

 
@property (nonatomic, strong) NSDate *publicationDate;
 

In
RSSItem.m
, synthesize this property and have the XML parsing methods seek out the appropriate element and put it into this property.

 
@synthesize publicationDate;
- (void)parser:(NSXMLParser *)parser
    didStartElement:(NSString *)elementName
       namespaceURI:(NSString *)namespaceURI
      qualifiedName:(NSString *)qualifiedName
         attributes:(NSDictionary *)attributeDict
{
    if ([elementName isEqual:@"title"]) {
        currentString = [[NSMutableString alloc] init];
        [self setTitle:currentString];
    }
    else if ([elementName isEqual:@"link"]) {
        currentString = [[NSMutableString alloc] init];
        [self setLink:currentString];
    }
else if ([elementName isEqualToString:@"pubDate"]) {
        // Create the string, but do not put it into an ivar yet
        currentString = [[NSMutableString alloc] init];
    }
}
- (void)parser:(NSXMLParser *)parser
 didEndElement:(NSString *)elementName
  namespaceURI:(NSString *)namespaceURI
 qualifiedName:(NSString *)qName
{
    
// If the pubDate ends, use a date formatter to turn it into an NSDate
    if ([elementName isEqualToString:@"pubDate"]) {
        static NSDateFormatter *dateFormatter = nil;
        if (!dateFormatter) {
            dateFormatter = [[NSDateFormatter alloc] init];
            [dateFormatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss z"];
        }
        [self setPublicationDate:[dateFormatter dateFromString:currentString]];
    }
    currentString = nil;
    if ([elementName isEqual:@"item"]
    || [elementName isEqual:@"entry"]) {
        [parser setDelegate:parentParserDelegate];
    }
}

(Wondering where we are pulling these crazy date formats from? The specification for the date format string is described at
http://unicode.org/reports/tr35/tr35-6.html#Date_Format_Patterns
. You can log the
pubDate
to the console to see its format.)

 

Of course, the
publicationDate
is something that needs to be cached with each
RSSItem
so that we can preserve the order of items the next time the application is run. Add code to the
NSCoding
methods in
RSSItem.m
to do this.

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

Now an
RSSItem
can be ordered amongst its peers by publication date. In
RSSChannel.m
, modify
addItemsFromChannel:
to reorder its items.

 
- (void)addItemsFromChannel:(RSSChannel *)otherChannel
{
    for (RSSItem *i in [otherChannel items]) {
        if (![[self items] containsObject:i])
            [[self items] addObject:i];
    }
    
    // Sort the array of items by publication date
    [[self items] sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
        return [[obj2 publicationDate] compare:[obj1 publicationDate]];
    }];
}

The method
sortUsingComparator:
is a nifty tool for sorting an
NSMutableArray
. This method takes a block that takes two object pointers as an argument and returns an
NSComparisonResult
. (An
NSComparisonResult
is a data type whose value can tell us the sorted order of two objects.) When this method executes, it compares each item in the array by passing two of them at a time as arguments to the supplied block. The block returns which of the two objects is ordered before the other as an
NSComparisonResult
.

 

The
NSDate
method
compare:
compares two
NSDate
instances and returns the one that is ordered before the other as an
NSComparisonResult
. The return value of
compare:
is the return value of the comparator block.

 

Now that a channel can update its
items
array when given a new channel, let’s have the store take advantage of this. In
BNRFeedStore.h
, change the return type of
fetchRSSFeedWithCompletion:
.

 
- (void)fetchRSSFeedWithCompletion:(void (^)(RSSChannel *obj, NSError *err))block
- (RSSChannel *)fetchRSSFeedWithCompletion:
                        (void (^)(RSSChannel *obj, NSError *err))block;
 

In
BNRFeedStore.m
, update the completion block for
fetchRSSFeedWithCompletion:
to merge the incoming channel with the existing channel and cache it.

 
- (void)fetchRSSFeedWithCompletion:(void (^)(RSSChannel *obj, NSError *err))block;
- (RSSChannel *)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];
    RSSChannel *channel = [[RSSChannel alloc] init];
    BNRConnection *connection = [[BNRConnection alloc] initWithRequest:req];
    
[connection setCompletionBlock:block];
    NSString *cachePath =
        [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
                                             NSUserDomainMask,
                                             YES) objectAtIndex:0];
    cachePath = [cachePath stringByAppendingPathComponent:@"nerd.archive"];
    // Load the cached channel
    RSSChannel *cachedChannel =
                [NSKeyedUnarchiver unarchiveObjectWithFile:cachePath];
    // If one hasn't already been cached, create a blank one to fill up
    if (!cachedChannel)
        cachedChannel = [[RSSChannel alloc] init];
    [connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
        // This is the store's callback code
        if (!err) {
            [cachedChannel addItemsFromChannel:obj];
            [NSKeyedArchiver archiveRootObject:cachedChannel
                                        toFile:cachePath];
        }
        // This is the controller's callback code
        block(cachedChannel, err);
    }];
    [connection setXmlRootObject:channel];
    [connection start];
    
return cachedChannel;
}

Other books

The Hammer of Eden by Ken Follett
Billy Summers by Stephen King
Nanny 911 by Julie Miller
The Awakening by Jana DeLeon
Némesis by Louise Cooper
Maid for the Billionaire by Ruth Cardello
Driving Lessons: A Novel by Fishman, Zoe
Outwitting Trolls by William G. Tapply