Silver Challenge: Favorites


#1

This is the solution I came up with. In it, I try to leverage the FeedStore object to avoid having direct communication between the WebViewController and the ListViewController. In addition, this approach avoids an issue where the WebViewController may be displaying an item that is not listed in the ListViewController anymore.

In the UI, I decided to have a button on the navigation bar of the WebViewController with the title “Add To Favorites” or “Remove From Favorites” depending on whether the item is or isn’t already marked as favorite. The ListViewController cells display a green bullet to the right of the title of favorite items (See image at bottom of post)–This was done to implement a simple and quick way to mark items on the tableview using the built-in of the {UITableViewCell imageView]. For this purpose I made a quick green bullet in Photoshop and added it to the project.

Following the logic of marking items are “read”, items are marked/unmarked as Favorites via the FeedStore. Marking items as read does not require cross-view communication; however, in my solution, marking an item as Favorite does. For this reason, when the FeedStore marks/unmarks an item as Favorite, it posts a notification. Other objects are free to listen for those and act accordingly.

First we add a new Entity to our CoreData data model. I named it Favorite and gave it the same property “urlString” as a Link Entity has. This simplifies the saving code below. This change to the CoreData model requires to either delete the old database in the application folder, or to make a mapping model. I just deleted the old db.

So here is the code.

In FeedStore.h we add some constants and method declarations:

extern NSString* const KFeedFavoritesChanged;
extern NSString* const KFeedFavoriteRemoved;
extern NSString* const KFeedFavoriteAdded;

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

And they are implemented in FeedStore.m. Some of the code necessary was redundant with marking items as read, so a couple of private methods consolidating all the redundant code were added. The reworked code is shown here:

NSString* const KFeedFavoritesChanged = @"KFeedFavoritesChanged";
NSString* const KFeedFavoriteRemoved = @"KFeedFavoriteRemoved";
NSString* const KFeedFavoriteAdded = @"KFeedFavoriteAdded";

-(void)markItemAsRead:(RSSItem*)item {
	if([self hasItemBeenRead:item]) {
		return;
	}
	[self saveItem:item forEntityName:@"Link"];
}

-(BOOL)hasItemBeenRead:(RSSItem*)item {
	return [self isItemInContext:item forEntityName:@"Link"];
}

-(void)markItemAsFavorite:(RSSItem*)item {
	if([self isItemFavorite:item]) {
		return;
	}
	// add item to context:
	[self saveItem:item forEntityName:@"Favorite"];
	// Notify of the change:
	NSDictionary *userInfo = [NSDictionary dictionaryWithObject:item forKey:KFeedFavoriteAdded];
	[[NSNotificationCenter defaultCenter] postNotificationName:KFeedFavoritesChanged 
														object:self 
													  userInfo];
}

-(void)unmarkItemAsFavorite:(RSSItem*)item {
	if(![self isItemFavorite:item]) {
		return;
	}
	NSFetchRequest	*req = [[NSFetchRequest alloc] initWithEntityName:@"Favorite"];
	NSPredicate		*pred = [NSPredicate predicateWithFormat:@"urlString like %@", [item link]];
	[req setPredicate:pred];
	NSArray *entries = [context executeFetchRequest:req error:nil];
	if([entries count]) {
		// remove the object from context and save:
		[context deleteObject:[entries objectAtIndex:0]];
		[context save:nil];
		// Notify of the change:
		NSDictionary *userInfo = [NSDictionary dictionaryWithObject:item forKey:KFeedFavoriteRemoved];	
		[[NSNotificationCenter defaultCenter] postNotificationName:KFeedFavoritesChanged 
															object:self 
														  userInfo];
	}
}

-(BOOL)isItemFavorite:(RSSItem*)item {
	return [self isItemInContext:item forEntityName:@"Favorite"];
}

// Private methods...
-(BOOL)isItemInContext:(RSSItem*)item forEntityName:(NSString*)entityName {
	NSFetchRequest	*req = [[NSFetchRequest alloc] initWithEntityName:entityName];
	NSPredicate		*pred = [NSPredicate predicateWithFormat:@"urlString like %@", [item link]];
	[req setPredicate:pred];
	NSArray *entries = [context executeFetchRequest:req error:nil];
	if([entries count] > 0) {
		return YES;
	}
	return NO;
}

-(void)saveItem:(RSSItem*)item forEntityName:(NSString*)entityName {
	NSManagedObject *obj = [NSEntityDescription insertNewObjectForEntityForName:entityName 
														 inManagedObjectContext:context];
	[obj setValue:[item link] forKey:@"urlString"];	
	[context save:nil];
}

In the WebViewController, we need to add a property to remember the RSSItem whose link we are displaying, add some code to the listViewController:handleObject: method and add a private method to handle the UIBarButtonItem click:

In WebViewController.h :

@property (nonatomic, strong) RSSItem *currentItem;

In WebViewController.m :

@synthesize currentItem;

-(void)listViewController:(ListViewController*)lvc handleObject:(id)object {
	if(![object isKindOfClass:[RSSItem class]]) {
		return;
	}
	
	if([[self currentItem] isEqual:object]) {
		return;
	}
	
	[self setCurrentItem:object];
	
	NSURL *url = [NSURL URLWithString:[[self currentItem] link]];
	NSURLRequest *req = [NSURLRequest requestWithURL:url];
	
	[[self webView] loadRequest:req];
	[[self navigationItem] setTitle:[[self currentItem] title]];
	
	// Add a Favorite button to the navigation item:
	NSString *favoriteButtonTitle = @"Add To Favorites";
	if([[FeedStore sharedStore] isItemFavorite:[self currentItem]]) {
		favoriteButtonTitle = @"Remove From Favorites";
	}
	UIBarButtonItem *favoriteButton = [[UIBarButtonItem alloc] initWithTitle:favoriteButtonTitle 
													    style:UIBarButtonItemStyleBordered 
													 target:self action:@selector(favoriteAcction)];
	[[self navigationItem] setRightBarButtonItem:favoriteButton];
}

// Handles navigation Favorite button click:
-(void)favoriteAcction {
	if(![[FeedStore sharedStore] isItemFavorite:[self currentItem]]) {
		[[FeedStore sharedStore] markItemAsFavorite:[self currentItem]];
		[[[self navigationItem] rightBarButtonItem] setTitle:@"Remove From Favorites"];
	} else {
		// remove from favorites here...
		[[FeedStore sharedStore] unmarkItemAsFavorite:[self currentItem]];
		[[[self navigationItem] rightBarButtonItem] setTitle:@"Add To Favorites"];
	}
}

Finally. In ListViewController, we add code to create the favorite icon image, listen to the favorite notification from the FeedStore, and handle the notification:

In ListViewController.h, add an instance variable for the new favorite icon:

	UIImage *favoriteIcon;

ListViewController.m add to -(id)initWithStyle: just before calling [self fetchEntries];

		favoriteIcon = [UIImage imageNamed:@"favorite_bullet.png"];
		[[NSNotificationCenter defaultCenter] addObserver:self 
												 selector:@selector(favoriteAction:) 
													 name:KFeedFavoritesChanged 
												   object:[FeedStore sharedStore]];

ListViewController.m add to tableView:cellForRowAtIndexPath: after setting the cell accessory type to indicate if the item has been read:

	if([[FeedStore sharedStore] isItemFavorite:item]) {
		[[cell imageView] setImage:favoriteIcon];
		[[cell imageView] setHighlightedImage:favoriteIcon];
	} else {
		[[cell imageView] setImage:nil];
		[[cell imageView] setHighlightedImage:nil];
	}

ListViewController.m add a private handler for the favorite notification:

-(void)favoriteAction:(NSNotification*)note {
	// If a row was selected, save reference to its path:
	NSIndexPath *indexPath = [[self tableView] indexPathForSelectedRow];
	// Now simply reload the table to reflect favorite changes:
	[[self tableView] reloadData];
	// If there was a selected row, reselect it:
	if(indexPath) {
		[[self tableView] selectRowAtIndexPath:indexPath 
									  animated:NO 
								scrollPosition:UITableViewScrollPositionNone];
	}
}

That’s it.