Please note, this blog entry is from a previous course. You might want to check out the current one.
Create a Core Data-managed object database model in Xcode suitable for storing any information you queried from Flickr in the last assignment that you feel you will need to perform the rest of the Required Tasks in this assignment. Take the time to give some thought to what Entities, Attributes and Relationships your database will need before you dive in.
Following the recommendations of the assignment create a new blank universal single-view project. Create a new data model. Start by adding two new Entities “Photo” and “Tag” to hold the photos and its tags. Add a many-to-many relationship between them, because a photo can have multiple tags and a tag can have multiple photos.
The first table view should show the name of the tag and the number of photos it is associated with. Create a new string attribute for the tag entity to hold the name. The number of photos will be derived using its relationship to the photo tags.
The second table view should show a title and subtitle for each photo. Add two string attributes for this to the photo entity. … and another one to hold the unique identifier to ensure to keep only one instance per photo. This table view provides the URL of the photo to the image view. Thus the photo entity needs another string attribute to hold the URL. Task #3 needs to store the thumbnail for which we add another attribute of the type binary data.
Create another entity to represent the recently-viewed-photos table view. It contains a single attribute to hold the date of the last view and a relationship to the photo.
Create NSManagedObject subclasses for all entities and categories for Photo and Tag.
The Photo+Flickr category looks similar to the one from the lecture demo – with slight adjustments. When adding a new photo – check if it already exisits. If it doesn’t fill its attributes using the data from the photo. For the tags use a new methods which will be added in the Tag category below:
+ (Photo *)photoWithFlickrInfo:(NSDictionary *)photoDictionary inManagedObjectContext:(NSManagedObjectContext *)context { Photo *photo = nil; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Photo"]; request.predicate = [NSPredicate predicateWithFormat:@"unique = %@", [photoDictionary[FLICKR_PHOTO_ID] description]]; NSError *error = nil; NSArray *matches = [context executeFetchRequest:request error:&error]; if (!matches || ([matches count] > 1) || error) { NSLog(@"Error in photoWithFlickrInfo: %@ %@", matches, error); } else if (![matches count]) { photo = [NSEntityDescription insertNewObjectForEntityForName:@"Photo" inManagedObjectContext:context]; photo.unique = [photoDictionary[FLICKR_PHOTO_ID] description]; photo.title = [photoDictionary[FLICKR_PHOTO_TITLE] description]; photo.subtitle = [[photoDictionary valueForKeyPath:FLICKR_PHOTO_DESCRIPTION] description]; photo.imageURL = [[FlickrFetcher urlForPhoto:photoDictionary format:FlickrPhotoFormatLarge] absoluteString]; photo.tags = [Tag tagsFromFlickrInfo:photoDictionary inManagedObjectContext:context]; } else { photo = [matches lastObject]; } return photo; }
The Tag+Flickr category works analogues. First generate an array of possible tag strings, and prepare the fetch request (it’s only necessary to do that once as it will be the save for all tags). Create mutable set to store all valid tags. Loop over all strings and skip empty strings or tag the tag names cs193pspot, portrait, and landscape. If a tag does already exist, just put it into the set. If not create it and fill its attributes. Finally return the set of tags:
+ (NSSet *)tagsFromFlickrInfo:(NSDictionary *)photoDictionary inManagedObjectContext:(NSManagedObjectContext *)context { NSArray *tagStrings = [photoDictionary[FLICKR_TAGS] componentsSeparatedByString:@" "]; if (![tagStrings count]) return nil; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Tag"]; request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]]; NSArray *matches; NSError *error; Tag *tag; NSMutableSet *tags = [NSMutableSet setWithCapacity:[tagStrings count]]; for (NSString *tagString in tagStrings) { if (!tagString) continue; if ([tagString isEqualToString:@"cs193pspot"]) continue; if ([tagString isEqualToString:@"portrait"]) continue; if ([tagString isEqualToString:@"landscape"]) continue; tag = nil; error = nil; request.predicate = [NSPredicate predicateWithFormat:@"name = %@", tagString]; 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 = tagString; } else { tag = [matches lastObject]; } if (tag) [tags addObject:tag]; } return tags; }
In the demo from the lecture we had only views where we could pass the “connection” to the core data from one to the next one using segues. Here we have multiple tabs where one tab does not care or even know, if another tab has been selected. One solution is to use shared document handler, which ensures that all tabs use the same “connection” or better document.
Create a new class with a public property for the NSManagedObjectContext, which will be used as “link” to the common document, a class method to create single common instance, and an instance method to connect to the document:
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext; + (SharedDocumentHandler *)sharedDocumentHandler; - (void)useDocument;
The class method creates a singleton of class and ensure that there always we only a single common instance:
+ (SharedDocumentHandler *)sharedDocumentHandler { static dispatch_once_t pred = 0; __strong static SharedDocumentHandler *_sharedDocumentHandler = nil; dispatch_once(&pred, ^{ _sharedDocumentHandler = [[self alloc] init]; }); return _sharedDocumentHandler; }
When using the document, we need first to create an URL/path to the document. Check if does already exist. Create it if does not. Open it if it closed. Or just use it. In any case we save our link to the document in the common managedObjectContext property.
- (void)useDocument { NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; url = [url URLByAppendingPathComponent:@"SPoTDocument"]; UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:url]; if (![[NSFileManager defaultManager] fileExistsAtPath:[url path]]) { [document saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) { self.managedObjectContext = document.managedObjectContext; }]; } else if (document.documentState == UIDocumentStateClosed) { [document openWithCompletionHandler:^(BOOL success) { self.managedObjectContext = document.managedObjectContext; }]; } else { self.managedObjectContext = document.managedObjectContext; } }
Finally, check if every thing works by using the current view controller. Load photos from Flickr and add them to core data:
- (void)viewDidLoad { [super viewDidLoad]; SharedDocumentHandler *sh = [SharedDocumentHandler sharedDocumentHandler]; [sh useDocument]; dispatch_queue_t queue = dispatch_queue_create("Flickr Downloader", NULL); dispatch_async(queue, ^{ NSArray *photos = [FlickrFetcher stanfordPhotos]; [sh.managedObjectContext performBlock:^{ for (NSDictionary *photo in photos) { [Photo photoWithFlickrInfo:photo inManagedObjectContext:sh.managedObjectContext]; } }]; }); }
When everything works find the SQLite file of your application in the simulator data. It should be in your home directory in “Library/Application Support/iPhone Simulator/6.1/Applications/ALONGCRYPTICNUMBER/Documents/SPoTDocument/” and is called persistentStore. Please note depending on which simulator version you use the link might be different. The best way to find the path is to log the URL when you create it in the shared-document handler.
The complete code is available on github.