Silver challenge - mark item as favorite


#1

In order to do this challenge, I first created a property of RSSItem :

(to be synthesized in the RSSItem.m)

Then I imported two image files : star_full.png and star_empty.png (48x48)

I subclassed the UITableViewCell as explained in chapter 15, creating a XIB file with a label and a UIImage for the star.
Also, I put a transparent UIButton on top of the UIImage, plugged at (IBAction)setFavorite:(id)sender

I called this new class NerdfeedItemCell :

NerdfeedItemCell.h

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

@interface NerdfeedItemCell : UITableViewCell

@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UIImageView *starView;

@property (weak, nonatomic) id controller;
@property (weak, nonatomic) UITableView *tableView;

// SILVER Challenge - mark item as favorite

  • (IBAction)setFavorite:(id)sender;

@end[/code]

NerdfeedItemCell.m

[code]#import “NerdfeedItemCell.h”

@implementation NerdfeedItemCell
@synthesize titleLabel;
@synthesize starView;
@synthesize controller, tableView;

// SILVER Challenge - mark item as favorite

  • (IBAction)setFavorite:(id)sender
    {
    // Get the name of this method, "setFavorite"
    NSString *selector = NSStringFromSelector(_cmd);
    // Append "atIndexPath"
    selector = [selector stringByAppendingString:@“atIndexPath:”];

    // Prepare a selector from this string
    SEL newSelector = NSSelectorFromString(selector);

    NSIndexPath *indexPath = [[self tableView] indexPathForCell:self];

    if (indexPath) {
    if ([[self controller] respondsToSelector:newSelector]) {
    [[self controller] performSelector:newSelector
    withObject:sender
    withObject:indexPath];
    }
    }

}
@end
[/code]

I then updated ListViewController.m as follows :

[code]- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
RSSItem *item = [[channel items]objectAtIndex:[indexPath row]];

// Get the new or recycled cell
NerdfeedItemCell *cell = [tableView dequeueReusableCellWithIdentifier:@"NerdfeedItemCell"];

[cell setController:self];
[cell setTableView:tableView];

[[cell titleLabel] setText:[item title]];


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

// SILVER Challenge - mark item as favorite
if ([[BNRFeedStore sharedStore]isItemFavorite:item]) {
    [[cell starView] setImage:[UIImage imageNamed:@"star_full"]];
} else {
    [[cell starView] setImage:[UIImage imageNamed:@"star_empty"]];
}

return cell;

}
[/code]

[code]// SILVER Challenge - mark item as favorite

  • (void)setFavorite:(id)sender atIndexPath:(NSIndexPath *)ip
    {
    NSLog(@“Set the item as favorite”);

    // get the item for the indexPath
    RSSItem *i = [[channel items]objectAtIndex:[ip row]];
    if ([i favorite] == YES) {
    [i setFavorite:NO];
    } else {
    [[BNRFeedStore sharedStore] markItemAsFavorite:i];
    [i setFavorite:YES];

    }

    [[self tableView] reloadData];
    }
    [/code]

Finally, to save the favorite property in CoreData, I updated Nerdfeed.xcdatamodeld by creating a new BestLink entity, with urlString as attribute (unique identifier for the topic).

Therefore, I updated BNRFeedStore.h this way :

[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]

and BNRFeedStore.m

[code]// SILVER Challenge - mark item as favorite

  • (void)markItemAsFavorite:(RSSItem *)item
    {
    // If the item is already in CoreData, it means we want to unmark the favorite
    if ([self isItemFavorite:item]) {
    NSFetchRequest *req = [[NSFetchRequest alloc] initWithEntityName:@“BestLink”];
    NSPredicate *pred = [NSPredicate predicateWithFormat:@“urlString like %@”, [item link]];
    [req setPredicate:pred];

      // If there is at least one Link, then this item has been marked as a favorite before
      NSArray *entries = [context executeFetchRequest:req error:nil];
      
      // delete object from context
      for (NSManagedObject *obj in entries)
          [context deleteObject:obj];
      return;
    

    }

    // Create a new Link object and insert it into the context
    NSManagedObject *obj = [NSEntityDescription insertNewObjectForEntityForName:@"BestLink"
    inManagedObjectContext:context];

    // Set the Link’s urlString from the RSSItem
    [obj setValue:[item link] forKey:@“urlString”];

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

  • (BOOL)isItemFavorite:(RSSItem *)item
    {
    // Create a request to fetch all Link’s with the same urlString as
    // this items link
    NSFetchRequest *req = [[NSFetchRequest alloc] initWithEntityName:@“BestLink”];

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

    // If there is at least one Link, then this item has been marked as a favorite before
    NSArray *entries = [context executeFetchRequest:req error:nil];
    if ([entries count] > 0)
    return YES;

    // If Core Data has never seen this link, then it hasn’t yet been marked as favorite
    return NO;
    }

[/code]

[EDIT : a few corrections in method markItemAsFavorite:, in order to allow to unmark the item as favorite by clicking on the full star indicator -> returns to empty star]


#2

hi freddy, it is a neat solution. But not perfect. What if user wants to see the favourites even though it is offline?

In your solution, all the favourite items are cached and as long as the cache is updated, the favourite history will be gone.

I think in this case, Core Data is better than cache for this kind of purpose. You need to control add/update/delete on the favourites. thus I would recommend using Core data for the favourite items instead of cached(NSArchived).

-lorixx


#3

Isn’t he using CoreData? I think I don’t get your point.


#4

Isn’t he using CoreData? I think I don’t get your point.[/quote]

Think about if there is another view controller showing all the favourite items.

Those favourite items are existed in the NSArchived cache, but those cache is updated every time we send a new request to a server, for example, fetching different data every day or every 5 minutes. Then those favourite items are not persisted in the device.

What i suggest is create a new Entity for RSSItem, and save them to Core data, then no matter the local cache is refreshed or not, those favourite RSSItems are always persisted in the core data database.

Feel free to let me know any problem with this solution. Thanks!


#5

Hi lorixx

Thanks for your feedback, I always appreciate to receive suggestions to enhance my work.
But I think I did use CoreData, not NSArchive, to save the favorites. I proceeded exactly the same way as for the “mark as read” indicator in the book.
I did create a new entity called BestLink, with a urlattribute to identify each item with a unique identifier.
So favorites are kept even if I’m offline.

[EDIT : put the CoreData model definition in bold in my original post]

Could you give me more precision about what you would do instead ? Or in addition to that ?

Thanks !
Fred


#6

[quote=“lorixx”]hi freddy, it is a neat solution. But not perfect. What if user wants to see the favourites even though it is offline?

In your solution, all the favourite items are cached and as long as the cache is updated, the favourite history will be gone.

I think in this case, Core Data is better than cache for this kind of purpose. You need to control add/update/delete on the favourites. thus I would recommend using Core data for the favourite items instead of cached(NSArchived).

-lorixx[/quote]

I think lorixx is trying to say that you are saving your favorite RSS items as NSArchive as part of the cache. Right now you are only saving the urlString in CoreData so when the favorite RSS items are purged from the cache, the CoreData will point to a nonexistent item. He is suggesting saving the actual item in CoreData instead of just the urlString. Or atleast that is how I read it.

BNRFeedStore.m
I modified fetchRSSFeedWithCompletion to add and remove favorite items

[code]- (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];

RSSChannel *channel = [[RSSChannel alloc] init];

BNRConnection *connection = [[BNRConnection alloc] initWithRequest:req];

NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0];
cachePath = [cachePath stringByAppendingPathComponent:@"nerd.archive"];

RSSChannel *cachedChannel = [NSKeyedUnarchiver unarchiveObjectWithFile:cachePath];
if (!cachedChannel) {
    cachedChannel = [[RSSChannel alloc] init];
}

// silver challenge ch29 begin
NSFetchRequest *favReq = [[NSFetchRequest alloc] initWithEntityName:@"FavoriteCache"];
NSArray *favEntries = [context executeFetchRequest:favReq error:nil];
for (NSManagedObject *favObject in favEntries) {
    RSSItem *favItem = [[RSSItem alloc] init];
    [favItem setTitle:[favObject valueForKey:@"title"]];
    [favItem setLink:[favObject valueForKey:@"link"]];
    [favItem setSubforum:[favObject valueForKey:@"subforum"]];
    [favItem setPublicationDate:[favObject valueForKey:@"publicationDate"]];
    [cachedChannel addItem:favItem]; // does not add duplicates
}
// silver challenge ch29 end

RSSChannel *channelCopy = [cachedChannel copy];

[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
    // store callback
    if (!err) {
        [channelCopy addItemsFromChannel:obj]; // add new items, sorted, newer at top

        // bronze challenge ch29 begin
        RSSChannel *archiveCopy = [channelCopy copy];
        
        [archiveCopy pruneEntriesTo:[[archiveCopy items] count] - [favEntries count]]; // silver challenge ch29
        [archiveCopy pruneEntriesTo:100];
                    
        [NSKeyedArchiver archiveRootObject:archiveCopy toFile:cachePath];
        // bronze challenge ch29 end
    }
    // controller callback
    block(channelCopy, err);
}];

[connection setXmlRootObject:channel];
[connection start];

return  cachedChannel;

}
[/code]

BNRFeedStore.m
I added new silver challenge methods.

[code]- (NSArray *)fetchItemFromFavorite:(RSSItem *)item // silver challenge ch29
{
NSFetchRequest *req = [[NSFetchRequest alloc] initWithEntityName:@“Favorite”];

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

return [context executeFetchRequest:req error:nil];

}

  • (NSArray *)fetchItemFromFavoriteCache:(RSSItem *)item // silver challenge ch29
    {
    NSFetchRequest *req = [[NSFetchRequest alloc] initWithEntityName:@“FavoriteCache”];

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

    return [context executeFetchRequest:req error:nil];
    }

  • (void)markItemAsFavorite:(RSSItem *)item // silver challenge ch29
    {
    if (![self isFavorite:item]) {
    // set as favorite
    NSManagedObject *obj = [NSEntityDescription insertNewObjectForEntityForName:@“Favorite” inManagedObjectContext:context];
    [obj setValue:[item link] forKey:@“urlString”];
    [context save:nil];

      // save to favorite cache
      NSManagedObject *o = [NSEntityDescription insertNewObjectForEntityForName:@"FavoriteCache" inManagedObjectContext:context];
      [o setValue:[item title] forKey:@"title"];
      [o setValue:[item link] forKey:@"link"];
      [o setValue:[item subforum] forKey:@"subforum"];
      [o setValue:[item publicationDate] forKey:@"publicationDate"];
      [context save:nil];
    

    } else {
    NSArray *entries = [self fetchItemFromFavorite:item];
    if ([entries count] > 0) {
    [context deleteObject:[entries objectAtIndex:0]];
    }

      NSArray *e = [self fetchItemFromFavoriteCache:item];
      if ([e count] > 0) {
          [context deleteObject:[e objectAtIndex:0]];
      }
    

    }
    }

  • (BOOL)isFavorite:(RSSItem *)item // silver challenge ch29
    {
    NSArray *entries = [self fetchItemFromFavorite:item];
    if ([entries count] > 0) {
    return YES;
    }
    return NO;
    }[/code]

ListViewController.m
I subclassed UITableViewCell called FavoriteViewCell which contains UIImageView with transparent button as the favorite button. I chose to color the table row instead of change the icon.

[code]- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath // silver challenge ch29
{
RSSItem *i = [[channel items] objectAtIndex:[indexPath row]];
if ([[BNRFeedStore sharedStore] isFavorite:i]) {
[cell setBackgroundColor:[UIColor cyanColor]];
} else {
[cell setBackgroundColor:[UIColor whiteColor]];
}
}

  • (void)markFavorite:(id)sender // silver challenge ch29
    {
    // sender > UITableViewCellContentView > UITableViewCellScrollView > UITableViewCell
    UITableViewCell *cell = (UITableViewCell *)[[[sender superview] superview] superview]; // sender is button
    if ([cell isKindOfClass:[FavoriteViewCell class]]) {

      RSSItem *i = [[channel items] objectAtIndex:[[[self tableView] indexPathForCell:cell] row]];
      [[BNRFeedStore sharedStore] markItemAsFavorite:i];
      
      if ([[BNRFeedStore sharedStore] isFavorite:i]) {
          [cell setBackgroundColor:[UIColor cyanColor]];
      } else {
          [cell setBackgroundColor:[UIColor whiteColor]];
      }
    

    }
    }
    [/code]
    RSSChannel.m
    Modified addItemsFromChannel to fix bug with un-favorite-ed items[code]- (void)addItemsFromChannel:(RSSChannel *)otherChannel
    {
    // silver challenge ch29 begin
    // had to switch which object to use as oldest publication date because adding favorites feature
    // allows “new” items to be added in the middle but the VC can only handle items coming down from the top
    //NSDate *oldestItemDate = [[items lastObject] publicationDate]; // bronze challenge ch29
    NSDate *oldestItemDate = [[items firstObject] publicationDate];
    // silver challenge ch29 end

    if (!oldestItemDate) {
    oldestItemDate = [NSDate distantPast];
    }

    for (RSSItem *i in [otherChannel items]) {
    CH29BronzeLog(@“addItemsFromChannel.checkingItem: %@”, i);
    if (![[self items] containsObject:i]) { // auto calls rssitem isequal
    CH29BronzeLog(@“addItemsFromChannel.uniqueItem!”);

          // bronze challenge ch29 begin
          switch ([oldestItemDate compare:[i publicationDate]]) {
              // prevent adding items older than cache
              case NSOrderedAscending:    // ok to add
                  CH29BronzeLog(@"addItemsFromChannel.adding!");
                  [[self items] addObject:i];
                  break;
              case NSOrderedSame:         // same date
                  CH29BronzeLog(@"addItemsFromChannel.do_NOT_add: same date");
                  break;
              case NSOrderedDescending:   // older than current oldest item date
                  CH29BronzeLog(@"addItemsFromChannel.do_NOT_add: old date");
                  break;
          }
          // bronze challenge ch29 end
      }
    

    }

    [[self items] sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
    return [[obj2 publicationDate] compare:[obj1 publicationDate]];
    }];
    }[/code]
    RSSChannel.m
    new method called addItem because items is readonly

- (void)addItem:(RSSItem *)i // silver challenge ch29 { if (![[self items] containsObject:i]) { [[self items] addObject:i]; [[self items] sortUsingComparator:^NSComparisonResult(id obj1, id obj2) { return [[obj2 publicationDate] compare:[obj1 publicationDate]]; }]; } }