cs193p – Assignment #6 Part #3

cs193p – Assignment #6 Part #3

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:

cs193p – assignment #6 task #1 - add background fetch capability
add background fetch capability

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.

FacebooktwitterredditpinterestlinkedintumblrmailFacebooktwitterredditpinterestlinkedintumblrmail

5 thoughts on “cs193p – Assignment #6 Part #3”

      1. 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?

          1. Ok, I know what was going wrong.

            I was missing a ! for
            if (!error) in loadRecentPhotosOnCompletion method.

Leave a Reply

Your email address will not be published.