Silver Challenge Solution


#1

For this challenge I added a button to WebViewController so the user could set the page as a favorite or remove it as a favorite. Alert messages appear confirming if it is added or removed from favorites. The ListViewController is updated immediately to reflect this. I showed items as favorite in ListViewController by changing the colour of the cell.

Included below are files in which I made alterations. In addition a new attribute was added to the Link entity called favUrlString.

ListViewController.m

[code]#import “ListViewController.h”
#import “RSSChannel.h”
#import “RSSItem.h”
#import “WebViewController.h”
#import “ChannelViewController.h”
#import “BNRFeedStore.h”

@implementation ListViewController
@synthesize webViewController;

-(id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];

if (self) {

    UIBarButtonItem *bbi =
    [[UIBarButtonItem alloc] initWithTitle:@"Info"
                                     style:UIBarButtonItemStyleBordered
                                    target:self
                                    action:@selector(showInfo:)];
    
    [[self navigationItem] setRightBarButtonItem:bbi];
    
    UISegmentedControl *rssTypeControl = [[UISegmentedControl alloc]initWithItems:
                                          [NSArray arrayWithObjects:@"BNR", @"Apple", nil]];
    
    [rssTypeControl setSelectedSegmentIndex:0];
    [rssTypeControl setSegmentedControlStyle:UISegmentedControlStyleBar];
    [rssTypeControl addTarget:self action:@selector(changeType:) forControlEvents:UIControlEventValueChanged];
    [[self navigationItem]setTitleView:rssTypeControl];

    [self fetchEntries];
}
return self;

}

  • (void)showInfo:(id)sender
    {
    // Create the channel view controller
    ChannelViewController *channelViewController = [[ChannelViewController alloc]
    initWithStyle:UITableViewStyleGrouped];

    if ([self splitViewController]) {

      UINavigationController *nvc = [[UINavigationController alloc]
                                     initWithRootViewController:channelViewController];
      
      // Create an array with our nav controller and this new VC's nav controller
      NSArray *vcs = [NSArray arrayWithObjects:[self navigationController],
                      nvc,
                      nil];
      
      // Grab a pointer to the split view controller
      // and reset its view controllers array.
      [[self splitViewController] setViewControllers:vcs];
      
      // Make detail view controller the delegate of the split view controller - ignore this warning
      [[self splitViewController] setDelegate:channelViewController];
      
      // If a row has been selected, deselect it so that a row
      // is not selected when viewing the info
      NSIndexPath *selectedRow = [[self tableView] indexPathForSelectedRow];
      if (selectedRow)
          [[self tableView] deselectRowAtIndexPath:selectedRow animated:YES];
    

    } else {
    [[self navigationController] pushViewController:channelViewController
    animated:YES];
    }

    // Give the VC the channel object through the protocol message
    [channelViewController listViewController:self handleObject:channel];
    }

  • (void)changeType:(id)sender
    {
    rssType = [sender selectedSegmentIndex];
    [self fetchEntries];
    }

  • (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
    //if not in a split view controller (when using iPhone) then push the webviewcontroller onto navigation controller stack. If in splitviewcontroler (using iPad) then just let the uiSplitViewController to place the webviewcontroller on screen
    if (![self splitViewController]) {
    // Push the web view controller onto the navigation stack - this implicitly
    // creates the web view controller’s view the first time through
    [[self navigationController] pushViewController:webViewController animated:YES];
    }
    else {
    // We have to create a new navigation controller, as the old one
    // was only retained by the split view controller and is now gone
    UINavigationController *nav =
    [[UINavigationController alloc] initWithRootViewController:webViewController];

      NSArray *vcs = [NSArray arrayWithObjects:[self navigationController],
                      nav,
                      nil];
      
      [[self splitViewController] setViewControllers:vcs];
      
      // Make the detail view controller the delegate of the split view controller
      [[self splitViewController] setDelegate:webViewController];
    

    }

    // Grab the selected item
    RSSItem *entry = [[channel items] objectAtIndex:[indexPath row]];

    [[BNRFeedStore sharedStore]markItemAsRead:entry];

    //immediately add a checkmark to this row
    [[[self tableView]cellForRowAtIndexPath:indexPath]setAccessoryType:UITableViewCellAccessoryCheckmark];

    [webViewController listViewController:self handleObject:entry];
    }

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [[channel items]count];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:@“UITableViewCell”];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:@“UITableViewCell”];
}
RSSItem *item = [[channel items] objectAtIndex:[indexPath row]];
[[cell textLabel] setText:[item title]];

NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss z"];
NSString *dateString = [dateFormatter stringFromDate:[item publicationDate]];
[[cell detailTextLabel] setText:dateString];

if ([[BNRFeedStore sharedStore]hasItemBeenRead:item]) {
    [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
} else {
    [cell setAccessoryType:UITableViewCellAccessoryNone];
}

//changes the cell appearance based on if the item is a favorite
if ([[BNRFeedStore sharedStore]isItemFavorite:item]) {
    [[cell textLabel] setBackgroundColor:[UIColor redColor]];
    [[cell detailTextLabel] setBackgroundColor:[UIColor redColor]];
    [[cell textLabel] setTextColor:[UIColor whiteColor]];
    [[cell detailTextLabel] setTextColor:[UIColor whiteColor]];
} else {
    [[cell textLabel] setBackgroundColor:[UIColor clearColor]];
    [[cell detailTextLabel] setBackgroundColor:[UIColor clearColor]];
    [[cell textLabel] setTextColor:[UIColor blackColor]];
    [[cell detailTextLabel] setTextColor:[UIColor grayColor]];
}
return cell;

}

//had to use this method to get the cell background to change color correctly

  • (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
    {
    RSSItem *item = [[channel items] objectAtIndex:[indexPath row]];

    if ([[BNRFeedStore sharedStore]isItemFavorite:item]) {
    [cell setBackgroundColor:[UIColor redColor]];
    } else {
    [cell setBackgroundColor:[UIColor clearColor]];
    }

}

  • (void)fetchEntries
    {
    //Get hold of the segmented control that is currently in the title view
    UIView *currentTitleView = [[self navigationItem]titleView];

    //create an 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) {
    NSLog(@“Completion block called”);

      //when the request completes - success or failure - replce the activity indicator with the segmented control
      [[self navigationItem] setTitleView:currentTitleView];
      
      if (!err) {
          //if everything went ok, grab the channel object, and relod the table.
          channel = obj;
          
          [[self tableView]reloadData];
      } else {
          //if things went bad, show an alert view
          NSString *errorString = [NSString stringWithFormat:@"Fetch failed: %@", [err localizedDescription]];
          
          UIAlertView *av  =[[UIAlertView alloc]initWithTitle:@"Error" message:errorString delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
          [av show];
      }
    

    };

    //initiate the request
    if (rssType == ListViewControllerRSSTypeBNR) {
    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 {
    [[BNRFeedStore sharedStore]fetchTopSongs:10 withCompletion:completionBlock];
    }
    NSLog(@“Executing code at the end of fetchEntries”);
    }

  • (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)io
    {
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad)
    return YES;
    return io == UIInterfaceOrientationPortrait;
    }

@end
[/code]

Note the use of a new method - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath This was the only way I could get the cell background to appear correctly

WebViewController.h

[code]#import <Foundation/Foundation.h>
#import “ListViewController.h”

@class RSSItem;
//@class ListViewController;

@interface WebViewController : UIViewController <ListViewControllerDelegate, UISplitViewControllerDelegate>
{
RSSItem *entry;
}

@property (nonatomic, readonly)UIWebView *webView;

@end[/code]

WebViewController.m

[code]#import “WebViewController.h”
#import “RSSItem.h”
#import “BNRFeedStore.h”
#import “ListViewController.h”

@implementation WebViewController

-(id)init
{
self = [super init];
if (self) {
UIBarButtonItem *bbi = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addFavorite:)];

    [[self navigationItem] setRightBarButtonItem:bbi];
}
return self;

}

//method adds the current page as a favorite

  • (IBAction)addFavorite:(id)sender
    {
    if ([[BNRFeedStore sharedStore]isItemFavorite:entry]) {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Favorite Cancelled"
    message:@"You have now removed this page from your favorites list"
    delegate:nil
    cancelButtonTitle:@"OK"
    otherButtonTitles:nil];
    [alert show];
    } else {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Favorite Added"
    message:@"You have now added this page to your favorites list"
    delegate:nil
    cancelButtonTitle:@"OK"
    otherButtonTitles:nil];
    [alert show];
    }

    [[BNRFeedStore sharedStore]markItemAsFavorite:entry];

    //when I add a page as a favorite I want to update ListViewController straight away so it can change its cell appearance
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
    UINavigationController *navController = self.splitViewController.viewControllers[0];
    ListViewController *controller = (ListViewController *)navController.topViewController;
    [[controller tableView]reloadData];
    } else {
    UINavigationController *navController = self.navigationController.viewControllers[0];
    ListViewController *controller = (ListViewController *)navController;
    [[controller tableView]reloadData];
    }

}

  • (void)loadView
    {
    // Create an instance of UIWebView as large as the screen
    CGRect screenFrame = [[UIScreen mainScreen] applicationFrame];
    UIWebView *wv = [[UIWebView alloc] initWithFrame:screenFrame];
    // Tell web view to scale web content to fit within bounds of webview
    [wv setScalesPageToFit:YES];

    [self setView:wv];
    }

  • (UIWebView *)webView
    {
    return (UIWebView *)[self view];
    }

  • (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)io
    {
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad)
    return YES;
    return io == UIInterfaceOrientationPortrait;
    }

-(void)listViewController:(ListViewController *)lvc handleObject:(id)object
{
// Cast the passed object to RSSItem
entry = object;

// Make sure that we are really getting a RSSItem
if (![entry isKindOfClass:[RSSItem class]])
    return;

// Grab the info from the item and push it into the appropriate views
NSURL *url = [NSURL URLWithString:[entry link]];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
[[self webView] loadRequest:req];

[[self navigationItem] setTitle:[entry title]];

}

  • (void)splitViewController:(UISplitViewController *)svc
    willHideViewController:(UIViewController *)aViewController
    withBarButtonItem:(UIBarButtonItem *)barButtonItem
    forPopoverController:(UIPopoverController *)pc
    {
    // If this bar button item doesn’t have a title, it won’t appear at all.
    [barButtonItem setTitle:@“List”];

    // Take this bar button item and put it on the left side of our nav item.
    [[self navigationItem] setLeftBarButtonItem:barButtonItem];
    }

  • (void)splitViewController:(UISplitViewController *)svc
    willShowViewController:(UIViewController *)aViewController
    invalidatingBarButtonItem:(UIBarButtonItem *)button
    {
    // Remove the bar button item from our navigation item
    // We’ll double check that its the correct button, even though we know it is
    if (button == [[self navigationItem] leftBarButtonItem])
    [[self navigationItem] setLeftBarButtonItem:nil];
    }

@end
[/code]

BNRFeedStore.h

[code]#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@class RSSChannel;

@class RSSItem;

@interface BNRFeedStore : NSObject
{
NSManagedObjectContext *context;
NSManagedObjectModel *model;
}

@property (nonatomic, strong) NSDate *topSongsCacheDate;

  • (BNRFeedStore *)sharedStore;

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

  • (void)fetchTopSongs:(int)count withCompletion:(void (^)(RSSChannel *obj, NSError *err))block;

-(void)markItemAsRead:(RSSItem *)item;
-(BOOL)hasItemBeenRead:(RSSItem *)item;

-(void)markItemAsFavorite:(RSSItem *)item;
-(BOOL)isItemFavorite:(RSSItem *)item;

@end
[/code]

BNRFeedStore.m

[code]#import “BNRFeedStore.h”
#import “RSSChannel.h”
#import “BNRConnection.h”
#import “RSSItem.h”

@implementation BNRFeedStore

  • (BNRFeedStore *)sharedStore
    {
    static BNRFeedStore *feedStore = nil;
    if (!feedStore) {
    feedStore = [[BNRFeedStore alloc]init];
    }
    return feedStore;
    }

// new

  • (BOOL)hasItemBeenRead:(RSSItem *)item
    {
    //create a request to fetch all links with the same url string as this items link
    NSFetchRequest *req = [[NSFetchRequest alloc] initWithEntityName:@“Link”];

    NSPredicate *pred = [NSPredicate predicateWithFormat:@“urlString like %@”, [item link]];
    [req setPredicate:pred];

    //if there is at least one link, then this item has been read before
    NSArray *entries = [context executeFetchRequest:req error:nil];
    if([entries count] > 0) {
    return YES ;
    } else {
    //if core data has never seen this link, then it hasn’t been read
    return NO;
    }
    }

  • (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 sve the changes
    [context save:nil];
    }

-(BOOL)isItemFavorite:(RSSItem *)item
{
//create a request to fetch all links with the same url string as this items link
NSFetchRequest *req = [[NSFetchRequest alloc] initWithEntityName:@“Link”];

NSPredicate *pred = [NSPredicate predicateWithFormat:@"favUrlString like %@", [item link]];
[req setPredicate:pred];

//if there is at least one link, then this item has been read before
NSArray *entries = [context executeFetchRequest:req error:nil];
if([entries count] > 0) {
    return YES ;
} else {
    //if core data has never seen this link, then it hasn't been read
    return NO;
}

}

-(void)markItemAsFavorite:(RSSItem *)item
{
//if the item is already in core data, then we want to delete it from favorites
if([self isItemFavorite:item]) {
//create a request to fetch all links with the same url string as this items link
NSFetchRequest *req = [[NSFetchRequest alloc] initWithEntityName:@“Link”];

    NSPredicate *pred = [NSPredicate predicateWithFormat:@"favUrlString like %@", [item link]];
    [req setPredicate:pred];
    
    //create an array with the fav urls
    NSArray *fetchedEntries = [context executeFetchRequest:req error:nil];
    
    for (NSManagedObject *entry in fetchedEntries) {
        [context deleteObject:entry];
    }
    
} else {
    //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:@"favUrlString"];
}

//immediately save the changes
[context save:nil];

}

  • (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]];
      }
      
      // Create the managed object context
      context = [[NSManagedObjectContext alloc] init];
      [context setPersistentStoreCoordinator:psc];
      
      // The managed object context can manage undo, but we don't need it
      [context setUndoManager:nil];
    

    }
    return self;
    }

-(RSSChannel *)fetchRSSFeedWithCompletion:(void (^)(RSSChannel *, NSError *))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 executed.
NSString *cachePath =
[NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
                                     NSUserDomainMask,
                                     YES) objectAtIndex:0];

cachePath = [cachePath stringByAppendingPathComponent:@"nerd.archive"];

// Load the cached channel...
RSSChannel *cachedChannel = [NSKeyedUnarchiver unarchiveObjectWithFile:cachePath];
//...or create an empty one to fill up
if(!cachedChannel) {
    cachedChannel = [[RSSChannel alloc] init];
}

//create another instance of RSSChannel which will be a copy of chachedChannel.  The block below will add any new items to channelCopy and then in fetchEntries in ListViewController the 2 instances are compared against each other.  If channelCopy has any new items then this allows us to animate the new row appearing in the ListViewController.
RSSChannel *channelCopy = [cachedChannel copy];

// When the connection completes, this block from the controller will be executed.
[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
    if(!err) {
        [channelCopy addItemsFromChannel:obj];
        
        RSSChannel *savedToCacheChannel = [channelCopy copy];
        if ([[savedToCacheChannel items]count]>100) {
            NSRange r;
            r.location = 100;
            r.length = [[savedToCacheChannel items] count]-100;
            
            [[savedToCacheChannel items] removeObjectsInRange:r];
        }
        [NSKeyedArchiver archiveRootObject:savedToCacheChannel toFile:cachePath];
    }
    
    block(channelCopy, err);
}];

//let the empty channel parse the returning data from th webservice
[connection setXmlRootObject:channel];

//begin the connection
[connection start];

return cachedChannel;

}

  • (void)fetchTopSongs:(int)count withCompletion:(void (^)(RSSChannel *, NSError *))block
    {
    //construct the cache path
    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
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
    block(cachedChannel, nil);
    }];
    // Don’t need to make the request, just get out of this method
    return;
    }
    }
    }

    //prepare a request URL, including the argument from the controller
    NSString *requestString = [NSString stringWithFormat:@“http://itunes.apple.com/us/rss/topsongs/limit=%d/json”, count];

    NSURL *url = [NSURL URLWithString:requestString];

    //setup the connection as normal
    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];

    [connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
    //this is the stores completion code:
    //if everything went smoothly, save the channel to disk and set cach date
    if (!err) {
    [self setTopSongsCacheDate:[NSDate date]];
    [NSKeyedArchiver archiveRootObject:obj toFile:cachePath];
    }

      //this is the controller's completion code
      block(obj, err);
    

    }];

    //let the empty channel parse the returning data from th webservice
    [connection setJsonRootObject:channel];

    //begin the connection
    [connection start];
    }

  • (void)setTopSongsCacheDate:(NSDate *)topSongsCacheDate
    {
    [[NSUserDefaults standardUserDefaults] setObject:topSongsCacheDate
    forKey:@“topSongsCacheDate”];
    }

  • (NSDate *)topSongsCacheDate
    {
    return [[NSUserDefaults standardUserDefaults]
    objectForKey:@“topSongsCacheDate”];
    }

@end
[/code]