Please note, this blog entry is from a previous course. You might want to check out the current one.
Download Photos from Flickr using the background fetching API
Assignment #6 Task #7
Fetch the URLforRecentGeoreferencedPhotos from Flickr periodically (a few times an hour when your application is in the foreground and whenever the system will allow when it is in the background using the background fetching API in iOS). You must load this data into a Core Data database with whatever schema you feel you need to do the rest of this assignment.
Task #7 requires to use the background fetching API instead of using ephemeral sessions like we did in part #2.
Add the background-fetching capability to your target:
Adjust the Flickr fetch method to use another “background” helper method:
- (void)startFlickrFetch { [FlickrHelper startBackgroundDownloadRecentPhotosOnCompletion:^(NSArray *photos, void (^whenDone)()) { NSLog(@"%d photos fetched", [photos count]); if (whenDone) whenDone(); }]; }
As you might have noticed above, I don’t like cluttering the application delegate. Where we put everything into the application delegate during the lecture, I will but most of the code into the Flickr helper class. It’s only necessary to tell the application delegate what to do when and background event appears:
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { [FlickrHelper handleEventsForBackgroundURLSession:identifier completionHandler:completionHandler]; }
Create a public interface for those two methods in the Flickr helper class:
+ (void)startBackgroundDownloadRecentPhotosOnCompletion:(void (^)(NSArray *photos, void(^whenDone)()))completionHandler; + (void)handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler;
Before actually starting a new background download, check if there is none already in progress. Then add a download task, name it (set its description, because we need to be able to identify it later on), and save the completion handler:
#define FLICKR_FETCH_RECENT_PHOTOS @"Flickr Download Task to Download Recent Photos" + (void)startBackgroundDownloadRecentPhotosOnCompletion:(void (^)(NSArray *photos, void(^whenDone)()))completionHandler { FlickrHelper *fh = [FlickrHelper sharedFlickrHelper]; [fh.downloadSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { if (![downloadTasks count]) { NSURLSessionDownloadTask *task = [fh.downloadSession downloadTaskWithURL:[FlickrFetcher URLforRecentGeoreferencedPhotos]]; task.taskDescription = FLICKR_FETCH_RECENT_PHOTOS; fh.recentPhotosCompletionHandler = completionHandler; [task resume]; } else { for (NSURLSessionDownloadTask *task in downloadTasks) [task resume]; } }]; }
We need to make sure to use a common background-download session, which we set only once using the Flickr helper itself as delegate:
@property (strong, nonatomic) NSURLSession *downloadSession; ... #define FLICKR_FETCH @"Flickr Download Session" - (NSURLSession *)downloadSession { if (!_downloadSession) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _downloadSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration backgroundSessionConfiguration:FLICKR_FETCH] delegate:self delegateQueue:nil]; }); } return _downloadSession; }
… and because we need to store it somewhere we use a singleton for reuse:
+ (FlickrHelper *)sharedFlickrHelper { static dispatch_once_t pred = 0; __strong static FlickrHelper *_sharedFlickrHelper = nil; dispatch_once(&pred, ^{ _sharedFlickrHelper = [[self alloc] init]; }); return _sharedFlickrHelper; }
Don’t forget to define the property for the completion handler:
@property (copy, nonatomic) void (^recentPhotosCompletionHandler)(NSArray *photos, void(^whenDone)());
The second public helper function just needs to store the completion handler passed by event (note, this is a different completion handler than the one above).
@property (copy, nonatomic) void (^downloadBackgroundURLSessionCompletionHandler)(); ... + (void)handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { if ([identifier isEqualToString:FLICKR_FETCH]) { FlickrHelper *fh = [FlickrHelper sharedFlickrHelper]; fh.downloadBackgroundURLSessionCompletionHandler = completionHandler; } }
Because the Flickr helper acts as delegate we need to define it as such. An NSURLSessionDownloadDelegate requires four non-optional methods. Two of them are not needed now, but we still need to declare them:
@interface FlickrHelper() <NSURLSessionDownloadDelegate> ... - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { }
When a download task has finished, check if it is one we want to handle (using the description set above). Try to get photos from the downloaded JSON file and pass them to the previously stored completion handler (the one for the recent photos …):
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { if ([downloadTask.taskDescription isEqualToString:FLICKR_FETCH_RECENT_PHOTOS]) { NSDictionary *flickrPropertyList; NSData *flickrJSONData = [NSData dataWithContentsOfURL:location]; if (flickrJSONData) { flickrPropertyList = [NSJSONSerialization JSONObjectWithData:flickrJSONData options:0 error:NULL]; } NSArray *photos = [flickrPropertyList valueForKeyPath:FLICKR_RESULTS_PHOTOS]; self.recentPhotosCompletionHandler(photos, ^{ [self downloadTasksMightBeComplete]; }); } }
If there was a problem, just call the same mystical helper function used above in the completion handler. We will declare it in a moment:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { if (error && (session == self.downloadSession)) { NSLog(@"Flickr background download session failed: %@", error.localizedDescription); [self downloadTasksMightBeComplete]; } }
No matter if the download finished with or without problems, call the download completion handler (after you have reset it, thus it needs another little helper itself):
- (void)downloadTasksMightBeComplete { if (self.downloadBackgroundURLSessionCompletionHandler) { [self.downloadSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { if (![downloadTasks count]) { void (^completionHandler)() = self.downloadBackgroundURLSessionCompletionHandler; self.downloadBackgroundURLSessionCompletionHandler = nil; if (completionHandler) { completionHandler(); } } }]; } }
Running the code in its current state, you should not really see a difference to the previous part. However, if you start the app and close it immediately using the home button before the download has finished, you will see the log message of the finished download shortly after.
The complete code for tasks #1 to #7 is available on github.
I fetched 250 photos by background fetch but 0 photos by foreground method in task 1.
Is this normal? (・・?)
Could you clarify what you mean?
Ok, sorry. I fetch always 0 photo by using the method of part 2.
But with the background fetch, I succeed to fetch 250 photos.
Is it normal?
Having run the code last time about a year ago, I don’t think so.
Ok, I know what was going wrong.
I was missing a ! for
if (!error) in loadRecentPhotosOnCompletion method.