Please note, this blog entry is from a previous course. You might want to check out the current one.
The Tag Search tab must bring up a list of all the tags found in all the photos that the user has chosen to be a part of this Virtual Vacation (sorted with most-often-seen first). Touching on a tag brings up a list of all the photos in the Virtual Vacation that have that tag. The tags in a Flickr photo dictionary are contained in a single, space- separated, all lowercase string (accessible via the FLICKR_TAGS key). Separate out and capitalize each tag (not all caps, just capitalize the first letter) so that they look nicer in the UI. Don’t include any tags that have a colon in them.
In both storyboards add an additional table view controller as well as an subclass for it. Create a push segue from the tags cell from the vacation view controller to the new controller and from its cell a another push segue to the vacation-photos table view controller. Adjust the cell reuse identifier and set the identifiers for the two new segues.
The new table view controller – the tags table view controller – has as model the name of the vacation for which it has to display its tags. Its setter is used to set the title of the view:
// TagsTableViewController.h @property (nonatomic, strong) NSString *vacation; // TagsTableViewController.m @synthesize vacation = _vacation; - (void)setVacation:(NSString *)vacation { if (vacation == _vacation) return; _vacation = vacation; self.title = [@"Tags of " stringByAppendingString:vacation]; }
Its value is set in the vacation table view controller class:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { .... if ([segue.identifier isEqualToString:@"Show Tags"]) { [segue.destinationViewController setVacation:self.vacation]; } }
The tags will be provided by Core Data thus make the new class a subclass of the CoreDataTableViewController class. Using the Visual Map add a new entity “Photo” and add a string attribute for the “name” of the tag and an integer attribute “count” to hold the number of photos associated with a tag. The later is necessary to sort the table because Core Data can only sort attributes. Add relationship between the Photo and the Tag entities. For Photo call it “tags”, for Tag “photos” and change both to “To-Many relationship”. Select all entities and recreate their NSManagedObject subclasses.
The fetched-results controller looks similar to the previous ones. For sorting we use the count attribute.
- (void)setupFetchedResultsController { NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Tag"]; request.sortDescriptors = [NSArray arrayWithObject: [NSSortDescriptor sortDescriptorWithKey:@"count" ascending:NO]]; self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:[VacationHelper sharedVacation:self.vacation].database.managedObjectContext sectionNameKeyPath:nil cacheName:nil]; } - (void)viewDidLoad { [super viewDidLoad]; [VacationHelper openVacation:self.vacation usingBlock:^(BOOL success) { [self setupFetchedResultsController]; }]; }
The table cell uses the capitalized name of the tag as title, and the number of photos as subtitle:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Tags Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; Tag *tag = [self.fetchedResultsController objectAtIndexPath:indexPath]; cell.textLabel.text = [tag.name capitalizedString]; cell.detailTextLabel.text = [NSString stringWithFormat:@"%d photos", [tag.photos count]]; return cell; }
For testing it is not necessary to adjust the helper method to create the database. But when the photo is added it has to include the tags:
+ (Photo *)photoFromFlickrInfo:(NSDictionary *)flickrInfo inManagedObjectContext:(NSManagedObjectContext *)context { .... photo.tags = [Tag tagsFromFlickrInfo:flickrInfo inManagedObjectContext:context]; .... }
To add the actual tags to the database, they need to be extracted from the the photo data first. If a tag is already in the database its count is increased by one, otherwise it is created.
+ (NSSet *)tagsFromFlickrInfo:(NSDictionary *)flickrInfo inManagedObjectContext:(NSManagedObjectContext *)context { NSArray *tagStrings = [[flickrInfo objectForKey:FLICKR_TAGS] componentsSeparatedByString:@" "]; if (![tagStrings count]) return nil; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Tag"]; request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]]; NSRange range; NSArray *matches; NSError *error; Tag *tag; NSMutableSet *tags = [NSMutableSet setWithCapacity:[tagStrings count]]; for (NSString *tagName in tagStrings) { tag = nil; if (!tagName || [tagName isEqualToString:@""]) continue; range = [tagName rangeOfString:@":"]; if (range.location != NSNotFound) continue; error = nil; request.predicate = [NSPredicate predicateWithFormat:@"name = %@", tagName]; matches = [context executeFetchRequest:request error:&error]; if (!matches || ([matches count] > 1) || error) { NSLog(@"Error in tagsFromFlickrInfo: %@ %@", matches, error); } else if (![matches count]) { tag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:context]; tag.name = tagName; tag.count = [NSNumber numberWithInt:1]; } else { tag = [matches lastObject]; tag.count = [NSNumber numberWithInt:[tag.count intValue] + 1]; } if (tag) [tags addObject:tag]; } return tags; }
Like in the last assignment, it is important to delete the old database if you change your Core Data model, otherwise the application will crash.
To display the photos for a tag the photo-vacation table view controller is reused. Thus it needs a new model to hold the chosen tag. Its setter is used to set the title and to make sure the place is not set (and vice versa in the place setter):
// VacationPhotosTableViewController.h @property (nonatomic, strong) Tag *tag; // VacationPhotosTableViewController.m @synthesize tag = _tag; - (void)setTag:(Tag *)tag { if (_tag == tag) return; _tag = tag; if (!_tag) return; self.title = tag.name; self.place = nil; }
The tag is set from the segue of the tags table view controller:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { [super prepareForSegue:segue sender:sender]; if ([segue.identifier isEqualToString:@"Show Vacation Photos"]) { [segue.destinationViewController setVacation:self.vacation]; NSIndexPath *indexPath = [self.tableView indexPathForCell:sender]; Tag *tag = [self.fetchedResultsController objectAtIndexPath:indexPath]; [segue.destinationViewController performSelector:@selector(setTag:) withObject:tag]; } }
To cope with the additional model the fetched-results controller has to be setup slightly different to provide the suitable predicate and context:
- (void)setupFetchedResultsController { NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Photo"]; request.sortDescriptors = [NSArray arrayWithObject: [NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES selector: @selector(localizedCaseInsensitiveCompare:)]]; NSManagedObjectContext *context; if (self.place) { request.predicate = [NSPredicate predicateWithFormat: @"place.name = %@", self.place.name]; context = self.place.managedObjectContext; } if (self.tag) { request.predicate = [NSPredicate predicateWithFormat: @"%@ in tags", self.tag]; context = self.tag.managedObjectContext; } self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:context sectionNameKeyPath:nil cacheName:nil]; }
The complete code for this task is available at github.