iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (97 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)
4.32Mb size Format: txt, pdf, ePub
Finishing the BNR feed

Now that an
RSSChannel
can be copied, you can create the third instance of
RSSChannel
for the store to use for merging the cached and new feeds. In
BNRFeedStore.m
, update
fetchRSSFeedWithCompletion:
.

 
    if (!cachedChannel)
        cachedChannel = [[RSSChannel alloc] init];
    
    RSSChannel *channelCopy = [cachedChannel copy];
    [connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
        if (!err) {
            
[cachedChannel addItemsFromChannel:obj];
            
[NSKeyedArchiver archiveRootObject:cachedChannel toFile:cachePath];
            
            [channelCopy addItemsFromChannel:obj];
            [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
        }
        
block(cachedChannel, err);
        
block(channelCopy, err);
    }];
 

We can now update
ListViewController
. In
ListViewController.m
, replace the block that is passed to
fetchRSSFeedWithCompletion:
in
fetchEntries
.

 
    if (rssType == ListViewControllerRSSTypeBNR) {
        
channel = [[BNRFeedStore sharedStore]
                    
fetchRSSFeedWithCompletion:completionBlock];
        
channel = [[BNRFeedStore sharedStore] fetchRSSFeedWithCompletion:
        ^(RSSChannel *obj, NSError *err) {
            // Replace the activity indicator.
            [[self navigationItem] setTitleView:currentTitleView];
            if (!err) {
                // How many items are there currently?
                int currentItemCount = [[channel items] count];
                // Set our channel to the merged one
                channel = obj;
                // How many items are there now?
                int newItemCount = [[channel items] count];
                // For each new item, insert a new row. The data source
                // will take care of the rest.
                int itemDelta = newItemCount - currentItemCount;
                if (itemDelta > 0) {
                    NSMutableArray *rows = [NSMutableArray array];
                    for (int i = 0; i < itemDelta; i++) {
                        NSIndexPath *ip = [NSIndexPath indexPathForRow:i
                                                             inSection:0];
                        [rows addObject:ip];
                    }
                    [[self tableView] insertRowsAtIndexPaths:rows
                            withRowAnimation:UITableViewRowAnimationTop];
                }
            }
        }];
        [[self tableView] reloadData];
    } else if(rssType == ListViewControllerRSSTypeApple)
        [[BNRFeedStore sharedStore] fetchTopSongs:10 withCompletion:completionBlock];

When the
ListViewController
makes a request, it will get back its cached copy as usual and update its table. When the request finishes, it will get the merged channel. It can count the number of items in each channel and determine how many new rows to add to the table view, animating each one in.

 

Build and run the application. Note the items currently in the
BNR
feed. Create a new post and refresh the feed. The new post will slide into the table view instead of just appearing out of nowhere. Pretty cool stuff.

 
Read and Unread Items

Now that you can cache both RSS feeds, let’s add another useful feature: when the user selects an
RSSItem
, we want the
ListViewController
to add an indicator to the table that shows that this item has been read (
Figure 29.7
). Even if the user closes the application, the list of read items should persist.
Nerdfeed
, then, needs to store the list of read
RSSItem
s on the filesystem.

 

Figure 29.7  RSSItems marked as read

 

The simplest way to add this feature would be to add a
hasBeenRead
Boolean instance variable to
RSSItem
. When the user tapped on a row, the corresponding
RSSItem
would change its value of
hasBeenRead
to
YES
. For each row in the table, a checkmark would be displayed if the corresponding
RSSItem
’s
hasBeenRead
variable is
YES
.

 

In normal circumstances, this is a great approach, but our plan for
Nerdfeed
is more ambitious. We’d like users to be able to use
Nerdfeed
on all of their iOS devices, and if an RSS item is read on one device, then that item should appear as read on every device. Another feature of iOS, iCloud, will allow
Nerdfeed
to do this with minimal effort.

 

We’ll get into the specifics of iCloud in the next chapter, but here’s a brief introduction. iCloud allows documents to be stored in a user’s iCloud account instead of in the application sandbox. These documents are then available to all of a user’s devices via the cloud. When one device changes one of those documents, every other device sees those changes, so the same application on other devices can read and modify the same document.

 

While we could store the cached
RSSItem
s in the cloud (along with a

hasBeenRead

flag), this would be a waste of space and bandwidth. The data for the
RSSItem
s is already available on another server, so there is no reason to redundantly store it in the user’s iCloud account. Instead, the only data that needs to be shared is the
link
of each item that has been read (
Figure 29.8
). (Remember, the
link
of an item is its URL and is what makes an item unique.)

 

Figure 29.8  Model diagram for read RSSItem

 

Core Data has iCloud support built in. An application can fetch and insert objects into a Core Data store in the cloud, and other devices will be informed of each change. Thus, maintaining the list of URLs with Core Data will be incredibly efficient: whenever an item is read, its URL will be inserted into the cloud. Other devices will download this small change instead of having to fetch the entire list of read items each time it changes.

 

In this section, we’ll take care of setting up Core Data and storing its SQLite database locally. In the next chapter, we will make the necessary changes to integrate iCloud support.

 

First, add the CoreData framework to the
Nerdfeed
target.

 

Next, add a Core Data model file to
Nerdfeed
and name it
Nerdfeed.xcdatamodeld
(
Figure 29.9
).

 

Figure 29.9  Adding a Core Data model file

 

In
Nerdfeed.xcdatamodeld
, add a new entity named
Link
and then add one
String
attribute named
urlString
(
Figure 29.10
).

 

Figure 29.10  The link entity

 

It will be
BNRFeedStore
’s job to manage interacting with Core Data (an external source of data). In
BNRFeedStore.h
, import the header for Core Data and add a forward declaration and a few new instance variables and methods.

 
#import
@class RSSChannel;
@class RSSItem;
@interface BNRFeedStore : NSObject
{
    NSManagedObjectContext *context;
    NSManagedObjectModel *model;
}
+ (BNRFeedStore *)sharedStore;
@property (nonatomic, strong) NSDate *topSongsCacheDate;
- (void)fetchTopSongs:(int)count
       withCompletion:(void (^)(RSSChannel *obj, NSError *err))block;
- (RSSChannel *)fetchRSSFeedWithCompletion:
                      (void (^)(RSSChannel *obj, NSError *err))block;
- (void)markItemAsRead:(RSSItem *)item;
- (BOOL)hasItemBeenRead:(RSSItem *)item;
@end
 

Before moving on with the implementation, let’s see how it is going to work. Currently, when the user taps on a row that corresponds to an
RSSItem
, the
ListViewController
loads that
RSSItem
into the
WebViewController
to display it. Now, the
ListViewController
will also send the message
markItemAsRead:
to the
BNRFeedStore
with the selected
RSSItem
. The store will grab the
link
from the
RSSItem
and insert it into Core Data.

 

Also, each time the
ListViewController
reloads its table, it will ask the store,

Has this item been marked as read?

The store will check with Core Data, and the
ListViewController
will place a checkmark in the row if the corresponding item has been read.

 

In
BNRFeedStore.m
, add an
init
method to create the appropriate objects for Core Data to work.

 
- (id)init
{
    self = [super init];
    if (self) {
        model = [NSManagedObjectModel mergedModelFromBundles:nil];
        NSPersistentStoreCoordinator *psc =
        [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
        NSError *error = nil;
        NSString *dbPath =
            [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                 NSUserDomainMask,
                                                 YES) objectAtIndex:0];
        dbPath = [dbPath stringByAppendingPathComponent:@"feed.db"];
        NSURL *dbURL = [NSURL fileURLWithPath:dbPath];
        if (![psc addPersistentStoreWithType:NSSQLiteStoreType
                               configuration:nil
                                         URL:dbURL
                                     options:nil
                                       error:&error]) {
            [NSException raise:@"Open failed"
                        format:@"Reason: %@", [error localizedDescription]];
        }
        context = [[NSManagedObjectContext alloc] init];
        [context setPersistentStoreCoordinator:psc];
        [context setUndoManager:nil];
    }
    return self;
}

This code – which is nearly the same as the code in
Chapter 16
– sets up the context for inserting and fetching objects. This context works with the persistent store coordinator and its SQLite persistent store to save and load objects to the filesystem.

 

Now on to implementing the two new methods on the store. First, import
RSSItem.h
into
BNRFeedStore.m
.

 
#import "RSSItem.h"
 

Then, in
BNRFeedStore.m
, implement
markItemAsRead:
to insert a new
Link
entity into Core Data with the URL of the
RSSItem
’s
link
.

 
- (void)markItemAsRead:(RSSItem *)item
{
    // If the item is already in Core Data, no need for duplicates
    if ([self hasItemBeenRead:item])
        return;
    // Create a new Link object and insert it into the context
    NSManagedObject *obj = [NSEntityDescription
                        insertNewObjectForEntityForName:@"Link"
                                 inManagedObjectContext:context];
    // Set the Link's urlString from the RSSItem
    [obj setValue:[item link] forKey:@"urlString"];
    // immediately save the changes
    [context save:nil];
}
 

In
BNRFeedStore.m
, implement
hasItemBeenRead:
to return
YES
if the
RSSItem
passed as an argument has its
link
stored in Core Data.

Other books

Scorpion Soup by Tahir Shah
Lovestruck Forever by Rachel Schurig
Christmas in the Trenches by Alan Wakefield
Absorbed by Emily Snow
In the Rearview by Maria Ann Green
Sybill by Ferguson, Jo Ann
Wallbanger by Sable Jordan
Robin Hood, el proscrito by Angus Donald
Rogue's Pawn by Jeffe Kennedy