cs193p – Assignment #6 Part #6

Please note, this blog entry is from a previous course. You might want to check out the current one.

We know already from task #7 that we need to store the photos using Core Data.

Assignment #6 Task #6

You must use UIManagedDocument to store all of your Core Data information. In other words, you cannot use the NSManagedObjectContext-creating code from the demo.


Use UIManagedDocument to Store Core Data

Hint #6

The fact that the UIManagedDocument opens/creates asynchronously has ramifications for your entire application design since its NSManagedObjectContext may not be ready the instant your application’s UI appears. Design your code as if it might take 10 seconds or more to open/create the document (meanwhile your UI is up, but empty, which is fine). It never will actually take that long, but your code should work if it did.

Let’s create another helper class with a single public method. Its task is to open/reuse a document and running a provided operation. The block will receive the document as well as a success flag:

+ (void)useDocumentWithOperation:(void (^)(UIManagedDocument *document, BOOL success))operation;

This class method gets a common – shared – document handler and tries to use the document:

+ (void)useDocumentWithOperation:(void (^)(UIManagedDocument *document, BOOL success))operation
{
    DocumentHelper *dh = [DocumentHelper sharedDocumentHelper];
    [dh useDocumentWithOperation:operation];
}

The shared document handler is a singleton, ensuring that there is always only one single object:

+ (DocumentHelper *)sharedDocumentHelper
{
    static dispatch_once_t pred = 0;
    __strong static DocumentHelper *_sharedDocumentHelper = nil;
    dispatch_once(&pred, ^{
        _sharedDocumentHelper = [[self alloc] init];
    });
    return _sharedDocumentHelper;
}

Theoretically, we could have accessed the document already in class method above. However, while a document is created or opened, it does not like to be opened by another process a second time. Thus we need to check if another process is already accessing (creating / opening) the document. In that case, we skip the current operation and try to rerun a little bit later. If there is no other process active, create, open, or just use the document:

#define DOCUMENT_NOT_READY_RETRY_TIMEOUT 1.0
- (void)useDocumentWithOperation:(void (^)(UIManagedDocument *document, BOOL success))operation
{
    UIManagedDocument *document = self.document;    
    if ([self checkAndSetPreparingDocument]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self performSelector:@selector(useDocumentWithOperation:)
                       withObject:operation afterDelay:DOCUMENT_NOT_READY_RETRY_TIMEOUT];
        });
    } else {
        if (![[NSFileManager defaultManager] fileExistsAtPath:[document.fileURL path]]) {
            [document saveToURL:document.fileURL
               forSaveOperation:UIDocumentSaveForCreating
              completionHandler:^(BOOL success) {
                  operation(document, success);
                  self.preparingDocument = NO;
              }];
        } else if (document.documentState == UIDocumentStateClosed) {
            [document openWithCompletionHandler:^(BOOL success) {
                operation(document, success);
                self.preparingDocument = NO;
            }];
        } else {
            BOOL success = YES;
            operation(document, success);
            self.preparingDocument = NO;
        }
    }
}

The object is stored in a property, and instantiated lazily:

@property (nonatomic, strong) UIManagedDocument *document;
...
#define DATABASE_DOCUMENT_NAME @"TopRegions"
- (UIManagedDocument *)document
{
    if (!_document) {
        NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
                                                             inDomains:NSUserDomainMask] lastObject];
        url = [url URLByAppendingPathComponent:DATABASE_DOCUMENT_NAME];
        _document = [[UIManagedDocument alloc] initWithFileURL:url];
    }
    return _document;
}

To know if another process is active, we use another property:

@property (nonatomic) BOOL preparingDocument;

We need to make sure that only one process can access this property at the same time:

- (BOOL)checkAndSetPreparingDocument
{
    static dispatch_queue_t queue;
    if (!queue) {
        queue = dispatch_queue_create("Flickr Helper Queue", NULL);
    }
    __block BOOL result = NO;
    dispatch_sync(queue, ^{
        if (!_preparingDocument) {
            _preparingDocument = YES;
        } else {
            result = YES;
        }
    });
    return result;
}

Database Schema

Now we have a way to access the document, it’s time to create a database schema:

cs193p – assignment #6 task #1 - database schema
database schema

Hint #7

Your schema needs to support the specific needs of your application. When you add a photo to the database, feel free to set attributes in Entities other than your Photo entity which can support the querying/sorting you need to do.

Hint #9

For example, you’ll likely want track directly the photographers who are active in a place or region (rather than relying on figuring it out from the photos all the time).

Add four entities to hold data about photos, photographers, regions, and recently selected photos.

For the photos we need string attributes to hold an unique identifier, a title and a subtitle, an image URL, and (because we peeked into future tasks) another one for the thumbnail URL as well as a data attribute for the thumbnail. A photographer can have multiple photos, thus add a many-to-one relation ship. A region will also have multiple photos, add another many-to-one relation ship. Finally add a one-to-one relationship to recent-photos entity.

The photographer entity needs a single string attribute to hold the name of the photographer. Because there will be multiple photographers for a region, and a photographer might have photos for multiple regions, add a many-to-many relationship.

The region needs two string attributes for its name and place ID.

Hint #8

With the proper schema, this entire application can be built with very straightforward sort descriptors and predicates. Put your brainpower into designing the right schema rather than building complicated predicates.

Hint #10

With the right schema, you should not need advanced KVC querying like @count. It’s perfectly fine to add an attribute to any entity that keeps track of a count of objects in a relationship if you want.

Add two integer attributes to store the numbers of photos and photographers for the region.

The recent entity gets a single date attribute.

Finally create subclasses for all four entities using Editor -> Create NSManagedObject Subclass …

Populate the Database

Instead of only logging that we have received photos, lets start using those photos:

- (void)application:(UIApplication *)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    [FlickrHelper loadRecentPhotosOnCompletion:^(NSArray *photos, NSError *error) {
        if (error) {
            ...
        } else {
            ...
            [self useDocumentWithFlickrPhotos:photos];
            completionHandler(UIBackgroundFetchResultNewData);
        }
    }];
}

- (void)startFlickrFetch
{
    [FlickrHelper startBackgroundDownloadRecentPhotosOnCompletion:^(NSArray *photos, void (^whenDone)()) {
        [self useDocumentWithFlickrPhotos:photos];
        if (whenDone) whenDone();
    }];
}

Use the document helper to open the document and save the photos into the database:

- (void)useDocumentWithFlickrPhotos:(NSArray *)photos
{
    [DocumentHelper useDocumentWithOperation:^(UIManagedDocument *document, BOOL success) {
        if (success) {
            [Photo loadPhotosFromFlickrArray:photos
                    intoManagedObjectContext:document.managedObjectContext];
            [document saveToURL:document.fileURL
               forSaveOperation:UIDocumentSaveForOverwriting
              completionHandler:nil];
        }
    }];
}

Note that we use a method of the Photo class. Every time the class files are created everything you change in those files will be lost. Therefore you need to use categories to extend those classes (Create a category using File -> New -> File… -> Objective-C category.

Because the photo class needs additional Flickr functionality, name the category Flickr, which should result in two new files Photo+Flickr.h/.m).

Create two public class methods:

+ (Photo *)photoWithFlickrInfo:(NSDictionary *)photoDictionary
        inManagedObjectContext:(NSManagedObjectContext *)context;
+ (void)loadPhotosFromFlickrArray:(NSArray *)photos // of Flickr NSDictionary
         intoManagedObjectContext:(NSManagedObjectContext *)context;

The class method called from the application delegate, just loops over the photo array and calls the other class method:

+ (void)loadPhotosFromFlickrArray:(NSArray *)photos // of Flickr NSDictionary
         intoManagedObjectContext:(NSManagedObjectContext *)context
{
    for (NSDictionary *photo in photos) {
        [self photoWithFlickrInfo:photo inManagedObjectContext:context];
    }
}

Search the database, if the photo has already been added. If not, create a new object and populate with the data from the photo:

+ (Photo *)photoWithFlickrInfo:(NSDictionary *)photoDictionary
        inManagedObjectContext:(NSManagedObjectContext *)context
{
    Photo *photo = nil;    
    NSString *unique = [FlickrHelper IDforPhoto:photoDictionary];
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Photo"];
    request.predicate = [NSPredicate predicateWithFormat:@"unique = %@", unique];    
    NSError *error;
    NSArray *matches = [context executeFetchRequest:request error:&error];    
    if (!matches || error || ([matches count] > 1)) {
        // handle error
    } else if ([matches count]) {
        photo = [matches firstObject];
        NSLog(@"%@ already in database", photo.title);
    } else {
        photo = [NSEntityDescription insertNewObjectForEntityForName:@"Photo"
                                              inManagedObjectContext:context];
        photo.unique = unique;
        photo.title = [FlickrHelper titleOfPhoto:photoDictionary];
        photo.subtitle = [FlickrHelper subtitleOfPhoto:photoDictionary];
        photo.imageURL = [[FlickrHelper URLforPhoto:photoDictionary] absoluteString];
        photo.thumbnailURL = [[FlickrHelper URLforThumbnail:photoDictionary] absoluteString];
        
        photo.photographer = [Photographer photographerWithName:[FlickrHelper ownerOfPhoto:photoDictionary]
                                         inManagedObjectContext:context];
        
        photo.region = [Region regionWithPlaceID:[FlickrHelper placeIDforPhoto:photoDictionary]
                                 andPhotographer:photo.photographer
                          inManagedObjectContext:context];
        NSLog(@"%@", photo.title);
    }    
    return photo;
}

Please note the log messages. We are using them to see if the code is working, because we will not yet populate the user interfaces …

Most of the used Flickr helper methods are from the previous assignments. However we need three new methods:

+ (NSURL *)URLforThumbnail:(NSDictionary *)photo;
+ (NSString *)ownerOfPhoto:(NSDictionary *)photo;
+ (NSString *)placeIDforPhoto:(NSDictionary *)photo;

… which are quite simple. They only the required data from the photo dictionary:

+ (NSURL *)URLforThumbnail:(NSDictionary *)photo
{
    return [FlickrHelper URLforPhoto:photo format:FlickrPhotoFormatSquare];
}

+ (NSString *)placeIDforPhoto:(NSDictionary *)photo
{
    return [photo valueForKeyPath:FLICKR_PHOTO_PLACE_ID];
}

+ (NSString *)ownerOfPhoto:(NSDictionary *)photo
{
    return [photo valueForKeyPath:FLICKR_PHOTO_OWNER];
}

For the photographer we need an additional class method for his/her creation. Create a new category for the photographer. Call it “Create” because it has to do with creation and define a public class method:

+ (Photographer *)photographerWithName:(NSString *)name
                inManagedObjectContext:(NSManagedObjectContext *)context;

Like before check first if the photographer already exists, create him/her and populate his/her attributes.

+ (Photographer *)photographerWithName:(NSString *)name
                inManagedObjectContext:(NSManagedObjectContext *)context
{
    Photographer *photographer = nil;
    if ([name length]) {
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Photographer"];
        request.predicate = [NSPredicate predicateWithFormat:@"name = %@", name];
        NSError *error;
        NSArray *matches = [context executeFetchRequest:request error:&error];
        if (!matches || ([matches count] > 1)) {
            // handle error
        } else if (![matches count]) {
            photographer = [NSEntityDescription insertNewObjectForEntityForName:@"Photographer"
                                                         inManagedObjectContext:context];
            photographer.name = name;
            NSLog(@"%@", photographer.name);
        } else {
            photographer = [matches lastObject];
            NSLog(@"%@ already in database", photographer.name);
        }
    }    
    return photographer;
}

… add another category for the region (“Create”) with another public class method:

+ (Region *)regionWithPlaceID:(NSString *)placeID
              andPhotographer:(Photographer *)photographer
       inManagedObjectContext:(NSManagedObjectContext *)context;

This one is slightly different, because it needs to adjust the count attributes, if there are more than one photo or photographer:

+ (Region *)regionWithPlaceID:(NSString *)placeID
              andPhotographer:(Photographer *)photographer
       inManagedObjectContext:(NSManagedObjectContext *)context
{
    Region *region = nil;
    if ([placeID length]) {
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Region"];
        request.predicate = [NSPredicate predicateWithFormat:@"placeID = %@", placeID];
        NSError *error;
        NSArray *matches = [context executeFetchRequest:request error:&error];
        if (!matches || ([matches count] > 1)) {
            // handle error
        } else if (![matches count]) {
            region = [NSEntityDescription insertNewObjectForEntityForName:@"Region"
                                                         inManagedObjectContext:context];
            region.placeID = placeID;
            region.photoCount = @1;
            [region addPhotographersObject:photographer];
            region.photographerCount = @1;
            NSLog(@"%@", region.placeID);
        } else {
            region = [matches lastObject];
            region.photoCount = @([region.photoCount intValue] + 1);
            
            if (![region.photographers member:photographer]) {
                [region addPhotographersObject:photographer];
                region.photographerCount = @([region.photographerCount intValue] + 1);;
            }
            NSLog(@"%@ already in database", region.placeID);
        }
    }

    return region;
}

Running the app, the log shows how new objects are generated or old ones reused:

cs193p – assignment #6 task #1 - populating database
populating database

The complete code for tasks #1 to #7 is available on github.

FacebooktwitterredditpinterestlinkedintumblrmailFacebooktwitterredditpinterestlinkedintumblrmail

8 thoughts on “cs193p – Assignment #6 Part #6”

  1. Some questions:
    The dh: sharedDocumentHelper instance will be initialized only once? because you use dispatch_once?

    why assigning 0 to pred
    static dispatch_once_t pred = 0; What’s the consequence if we don’t do this assignment?

    in checkAndSetPreparingDocument, why do this dispatch_sync(queue, ^{
    if (!_preparingDocument) {
    _preparingDocument = YES;
    } else {
    result = YES;
    }
    });
    synchronously?

    Let me make clear your logic
    if (!_preparingDocument) {
    _preparingDocument = YES;
    } else {
    result = YES;
    }
    if preparingDocument has been set to NO, it means another processing is already accessing the document. And we return result = NO, preparingDocument = YES.

    Then we delay DOCUMENT_NOT_READY_RETRY_TIMEOUT, reaccess to this document. This time we can access to it, because preparingDocument is YES.

    Provided that preparingDocument = YES, two process want to access to this document at almost the same time. process A arrived a little bit earlier, so he succeeded in accessing this document.

    But what if the process B arrive at the time before process A setting preparingDocument to NO? Is it possible?

    1. The predicate is used to test whether the block has completed or not. I guess not setting it would initialize it to 0 anyway.
      Concerning your second question, that’s why I use a synchronized process …

      1. OK, Thank you, I understand :D. Your preparingDocument is a valve. If a process is accessing to the document, its value is always YES, That means we close this valve until this process finish its access. That’s so cool!

  2. regionWithPlaceID

    Maybe I can understand this way:
    the Region entity until now stores the placeID, the number of photos for each placeID, the photographer and the number of photographers who took photo for each placeID.

      1. Sorry, I should have been more clear. Your Region entity here stores placeID, so region.name have not been extracted yet.

        Never mind, this is not a problem : )

  3. – (void)useDocumentWithOperation:(void (^)(UIManagedDocument *document, BOOL success))operation

    Can I understand this method this way?
    “useDocumentWithOperation” is the parent method of “operation” which has two argument “document” and “success”.

    And the class method “+ (void)useDocumentWithOperation:(void (^)(UIManagedDocument *document, BOOL success))operation” is used to initialize a singleton instance of “DocumentHelper” dh, so finally there can be only one “DocumentHelper ” instance which is dh.

Leave a Reply

Your email address will not be published.