Please note, this blog entry is from a previous course. You might want to check out the current one.
Download and Store Region Information
Hint #12
To get the name of a place’s region from a place_id, you will need to query Flickr again at the URLforInformationAboutPlace: (off the main queue, of course) using the FLICKR_PHOTO_PLACE_ID found in a photo dictionary and then pass the dictionary returned from that (converted from JSON) to extractRegionNameFromPlaceInformation:.
After we have stored the photo data, lets fetch and store the region data:
+ (void)loadPhotosFromFlickrArray:(NSArray *)photos // of Flickr NSDictionary intoManagedObjectContext:(NSManagedObjectContext *)context { ... [Region loadRegionNamesFromFlickrIntoManagedObjectContext:context]; }
Like before we need to extend the region class. We could use the Create category of the previous part, but it’s nicer to use a separate category Flickr.
Create a new category (Flickr) containing a single public class method:
+ (void)loadRegionNamesFromFlickrIntoManagedObjectContext:(NSManagedObjectContext *)context;
This method is not that complicated, may be a little bit tricky, that’s why I will split into smaller parts.
When we saved the photo data together with its references to photographers and regions, we created only a dummy region containing the place ID of the photo, but no further information about the region. Therefore we start by collecting all regions from Core Data, which have no name yet, and loop over them … In addition we use an indicator telling the following code to save the document, which will happen when the last region data has been stored. If you try to save the document, the individual save commands might overlap:
+ (void)loadRegionNamesFromFlickrIntoManagedObjectContext:(NSManagedObjectContext *)context { NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Region"]; request.predicate = [NSPredicate predicateWithFormat:@"name.length = %@", nil]; NSError *error; NSArray *matches = [context executeFetchRequest:request error:&error]; if (!matches) { // nothing to do ... } else { BOOL saveDocument = NO; for (Region *match in matches) { if ([match isEqual:[matches lastObject]]) { saveDocument = YES; } // handle region download ... } } }
The download for each region will happen using a background session (which we will define in the Flickr helper class). The helper method receives the place ID for the current region, as well as a completion handler which requires a string parameter containing the name of the region and another block which it should fire at the end:
[FlickrHelper startBackgroundDownloadRegionForPlaceID:match.placeID onCompletion:^(NSString *regionName, void (^whenDone)()) { // store region data ... }];
When the download has finished, it might be that the document is not ready (because it has not been opened yet, or it has been closed in the meantime), therefore use the helper method from the last part of this assignment. When this process succeeds, run the rest of our code in the context of the available document, otherwise just run the final completion handler:
[DocumentHelper useDocumentWithOperation:^(UIManagedDocument *document, BOOL success) { if (success) { [document.managedObjectContext performBlock:^{ // handle the region data ... if (whenDone) whenDone(); }]; } else { if (whenDone) whenDone(); } }];
Now (that’s when the region data has been downloaded, and the document is available). Get the region of the corresponding place ID from the data base (its name should be empty). Because a region can have multiple place IDs, there could (will) be other place IDs stored in the database. If they have already the current region name assigned, get them. (Theoretically there should be only one of them, if any, but assuming there are more, will add some fault tolerance). If there are none, just set the region name to the current region. If there are other regions (with the same name), move their photos and photographer to current region, and delete the additional – now unneeded – regions. … and adjust the counters for available photos and photographers. If the current region is the last region for which we started a background download – it might not actually be the last finishing download – save the document:
Region *region = nil; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Region"]; request.predicate = [NSPredicate predicateWithFormat:@"placeID = %@", match.placeID]; NSError *error; NSArray *matches = [document.managedObjectContext executeFetchRequest:request error:&error]; if (!matches || ([matches count] != 1)) { // handle error } else { region = [matches lastObject]; request.predicate = [NSPredicate predicateWithFormat:@"name = %@", regionName]; matches = [document.managedObjectContext executeFetchRequest:request error:&error]; if (!matches) { // handle error } else if (![matches count]) { NSLog(@"%@", regionName); region.name = regionName; } else { NSLog(@"%@ with existing other paces", regionName); region.name = regionName; for (Region *match in matches) { region.photos = [region.photos setByAddingObjectsFromSet:match.photos]; region.photoCount = @([region.photos count]); region.photographers = [region.photographers setByAddingObjectsFromSet:match.photographers]; region.photographerCount = @([region.photographers count]); [document.managedObjectContext deleteObject:match]; } } if (saveDocument) { [document saveToURL:document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil]; } }
The download is started via the Flickr helper using a public class method:
+ (void)startBackgroundDownloadRegionForPlaceID:(NSString *)placeID onCompletion:(RegionCompletionHandler)completionHandler;
… which is using a custom type to shorten block definitions:
typedef void (^RegionCompletionHandler) (NSString *regionName, void(^whenDone)());
The class method is quite similar to the one starting the photo background-download sessions. The only real difference (beside another task description), is that we store the completion handlers in a dictionary using the task identifier as keys. When a download task finishes, we will use its identifier to get the correct completion handler:
#define FLICKR_FETCH_REGION @"Flickr Download Task to Download Region" + (void)startBackgroundDownloadRegionForPlaceID:(NSString *)placeID onCompletion:(RegionCompletionHandler)completionHandler { FlickrHelper *fh = [FlickrHelper sharedFlickrHelper]; [fh.downloadSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { NSURLSessionDownloadTask *task = [fh.downloadSession downloadTaskWithURL:[FlickrFetcher URLforInformationAboutPlace:placeID]]; task.taskDescription = FLICKR_FETCH_REGION; [fh.regionCompletionHandlers setObject:[completionHandler copy] forKey:@(task.taskIdentifier)]; [task resume]; }]; }
Don’t forget to declare the dictionary property, and instantiate it lazily:
@property (strong, nonatomic) NSMutableDictionary *regionCompletionHandlers; ... - (NSMutableDictionary *)regionCompletionHandlers { if (!_regionCompletionHandlers) { _regionCompletionHandlers = [[NSMutableDictionary alloc] init]; } return _regionCompletionHandlers; }
When the download has finished, parse the returned JSON file, extract the name of the region and pass it to the matching completion handler (which we delete afterwards):
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { if ([downloadTask.taskDescription isEqualToString:FLICKR_FETCH_RECENT_PHOTOS]) { ... } else if ([downloadTask.taskDescription isEqualToString:FLICKR_FETCH_REGION]) { NSDictionary *flickrPropertyList; NSData *flickrJSONData = [NSData dataWithContentsOfURL:location]; if (flickrJSONData) { flickrPropertyList = [NSJSONSerialization JSONObjectWithData:flickrJSONData options:0 error:NULL]; } NSString *regionName = [FlickrFetcher extractRegionNameFromPlaceInformation:flickrPropertyList]; RegionCompletionHandler regionCompletionHandler = [self.regionCompletionHandlers[@(downloadTask.taskIdentifier)] copy]; if (regionCompletionHandler) { regionCompletionHandler(regionName, ^{ [self downloadTasksMightBeComplete]; }); } [self.regionCompletionHandlers removeObjectForKey:@(downloadTask.taskIdentifier)]; } }
Running the app in its current version for the first time, will show log messages of the fetched region names and indications that other places for the region existed in the data base. Reruns will show less log messages and more indications of existing regions …
The complete code for tasks #1 to #7 is available on github.
I am confused about the use of block.
does void(^whenDone)() equals to void(^)()whenDone ?
and the typedef void (^RegionCompletionHandler) (NSString *regionName, void(^whenDone)()) means that we define a type which name is RegionCompletionHandler.
So when we do the declaration (I am not sure if it’s a declaration): (RegionCompletionHandler)completionHandler, which equals to say void(^completionHandler)(NSString *regionName, void(^whenDone)()) right?
The different syntax seems (or seemed a year ago) to be necessary for the typedef. Yes, concerning the second/last question.
Some problems:
1) We have three “matches” in + (void)loadRegionNamesFromFlickrIntoManagedObjectContext:(NSManagedObjectContext *)context; method.
1. first for the request @”name.length = %@”, nil
2. second for the @”placeID = %@”,placeID
3. third for the @”name = “, regionName
And they all have the same name “matches”. So the latter “matches” would be flushed out by the former?
2) after finding the “matches” for the request.predicate @”placeID = %@”, placeID, you assign region = [matches lastObject]. What about other matches besides the lastObject?
I means if the elements in matches all have the same placeID, they should all belong to the same region. So they need to be all assigned a regionName?
1) Concerning “three matches”, the second and third ones are within a completion block, and have nothing to do with the first one, which might not even exist any more when the second one is declared. The third one only fires when the second one did not produce a result and thus I do not care anymore about the second one 😉 … but you are right, that might be not the best coding practice …
2) The/a place ID is stored for the region. A region can/will have multiple regions (which I collect in the code and reduce it to a single one), but a place can only be in a single region.
Ok, since the placeID is unique, the condition of [matches count] != 1 is regarded as error.
So here it’s not important which object we take from the Matches, because there is only one exists, so we just do this region = [matches lastObject];
Cool!
But with this request: request.predicate = [NSPredicate predicateWithFormat:@”name = %@”, regionName];
we fetch all the region(s) (matches) which name is “regionName”. If [matches count] >= 1, this matches should contains the “region = [matches lastObject]” (for clarity, I call it region1). Because the regionName is generated from the placeID that “region1” possess.
So in this condition, when we finish doing setByAddingOjectsFromSet:, we are going to have two photos sets of this “region1”.
I don’t know if I expressed clearly enough…
In that part of the code I collect all doublets into that very first region, which I address using “region1” from the above call
Yes, I understand your logic.
What I think is, “matches” includes region1 after executing the fetch request.
So before collecting all the doublets in to region1, we should probably exclude the region1 from matches.
No, because it is the the last region added it does not have a name yet set … and is not included in the matches …
Damn, you are right!