Work Records

日々の作業記録です。ソフトウェアエンジニアリング全般から、趣味の話まで。

Push通知を受けたらバックグラウンドで画像をダウンロードする時のまとめ

やりたい事

1. アプリがバックグラウンドで起動している時にPush通知を受け取る
2. バックグラウンドにいる間に、あるURLから画像をダウンロードしておく
3. 起動時には画像はダウンロード済みで、サクサク動いてうれしい!

UIBackgroundFetchResultNewDataではまった

Pushを受け取った後に、バックグラウンド処理をする為には、application:didReceiveRemoteNotification:fetchCompletionHandler:をAppDelegate.mに実装する。
以下、実装(一部抜粋)。

AppDelegate.m
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler
{
  if (application.applicationState == UIApplicationStateBackground) {

    // バックグラウンドで画像をダウンロードする用のClass
    ImageDownloadInBackground *imageDownloadInBackground = [[ImageDownloadInBackground alloc] init];
    [imageDownloadInBackground downloadByPushInBackground];

    completionHandler(UIBackgroundFetchResultNewData);
  }
}

- (void) application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
  // このメソッドを書いておくと、バックグラウンドでNSURLSessionが呼ばれていた場合、そのdelegate method(ダウンロードが完了した場合のcallbackとか)が一斉に呼び出される
  // 書いておかないと、アプリがforegroundになったときに一斉に呼ばれるので×
}
ImageDownloadInBackground.h
#import <Foundation/Foundation.h>

@interface ImageDownloadInBackground : NSObject <NSURLSessionDownloadDelegate>

@end
ImageDownloadInBackground.m
- (void) downloadByPushInBackground:(NSDictionary *)transitionInfo
{
  NSString *identifier = @"identifier";
  NSURLSessionConfiguration *configuration;
  if ([[[UIDevice currentDevice] systemVersion] floatValue] >=8.0f) {
    configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
  } else {
    configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:identifier];
  }
  configuration.allowsCellularAccess = YES;
  NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
  NSString *forceStringURL = [NSString stringWithFormat:@"%@", url];
  NSURL *assetURL = [NSURL URLWithString:forceStringURL];
  NSURLSessionDownloadTask *task = [session downloadTaskWithURL:assetURL];
  [task resume];
}

- (void) URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
  NSData *downloadedData = [NSData dataWithContentsOfURL:location];
  if ([downloadedData length] == 0) {
    return;
  }
  
  ~ donwloadedDataをごにょごにょにする処理 ~

}

これでうまくいくかと思いきや、completionHandler(UIBackgroundFetchResultNewData)をよんだ時点でデータの更新が正常に終わったと判断され、バックグラウンドの処理(NSURLSessionDownloadDelegateのdelegate methodたち)が呼ばれなくなってしまう。

completionHandlerをダウンロード終了後に呼ぶようにする

という事で、completionHandlerをImageDownloadInBackgroundクラスに渡して、ダウンロードが正常に終わってからUIBackgroundFetchResultNewDataを呼ぶように修正。
以下、変更点だけ。

AppDelegate.m
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler
{
  if (application.applicationState == UIApplicationStateBackground) {

    // バックグラウンドで画像をダウンロードする用のClass
    ImageDownloadInBackground *imageDownloadInBackground = [[ImageDownloadInBackground alloc] init];
    // completionHandlerをimageDownloadInBackgroundの保持する配列に突っ込む
    [imageDownloadInBackground.completionHandlerArray addObject:completionHandler];
    [imageDownloadInBackground downloadByPushInBackground];

    // ここでは呼ばない
    // completionHandler(UIBackgroundFetchResultNewData);
  }
}
ImageDownloadInBackground.h

completionHandlerを格納する為の配列を定義

#import <Foundation/Foundation.h>
 
typedef void (^CompletionHandlerType)();

@interface ImageDownloadInBackground : NSObject <NSURLSessionDownloadDelegate>
 
@property NSMutableArray *completionHandlerArray;
 
 @end
ImageDownloadInBackground.m

// 配列の初期化、completionHandlerを呼ぶ処理を追加

- (id) init {
  self = [super init];
  if (self != nil) {
    _completionHandlerArray = [[NSMutableArray alloc] init];
  }
  return self;
}

- (void) URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
  NSData *downloadedData = [NSData dataWithContentsOfURL:location];
  if ([downloadedData length] == 0) {
    return;
  }
  
  ~ donwloadedDataをごにょごにょにする処理 ~

  CompletionHandlerType handler = _completionHandlerArray[0];
  handler(UIBackgroundFetchResultNewData);
  [_completionHandlerArray removeAllObjects];
}

これでダウンロードが正常に完了するようになった。
めでたし。