もうデータダウンロードでユーザを待たせない!iOS バックグラウンドダウンロードの活用!

DSC_4258.jpg

こんにちは。テクコンサルのチョイです。

夏といえば海水浴場のパラソルの下でソーシャルゲームを遊ぶことを想像しますね。(リア充?)通信速度を考えてホテルのWi-Fiで予めゲームをダウンロードしておいたから、早速始めましょう。え!?更にデータをダウンロードするの?しょうがない。ほかのことしよう、と2度とゲームを立ち上げることがありませんでした。

海水浴場ではありませんが、私も会社でゲームをダウンロードしておいたのに、帰りの電車中で追加ダウンロードを求められた経験があります。みなさんはどうでしょう。

このように、ゲームを試しもせずに離脱してしまうなんてとても残念です。せめてゲームデータダウンロード中にユーザがほかのことを自由にしていただき、ダウンロードが終わったら通知できればユーザにデライトだと思います。

さてと、お題にあるバックグラウンドダウンロード機能についてですが、秋にリリース予定のiOS 8の新機能ではなく、なんとiOS 7から提供されています!2014年6月に開催されたWWDC 2014の発表によりますと、iOS 7のシェアは9割近くにきていますので、むしろ今が旬ではないでしょうか。早速(もうだいぶ遅いが)使ってみましょう。

iOSバックグランドダウンロードとよんでいますが、その実体はNSURLSessionを利用したBackground Transferになります。またこちらに、公式サンプルが提供されています。

Background Transferについて

Background Transferといいますと、難しそうに聞こえますが仕組みとその利用方法は極めて簡単です。主にはNSURLSessionNSURLSessionTask(抽象クラスですので実際にはその実装クラスNSURLSessionDownloadTaskNSURLSessionUploadTask)およびそれらのdelegateメソッドを利用します。

詳細については次のサンプル説明を参考にして下さい。

サンプル実装について

本ブログ記事では、公式サンプルを参考に下記のシーケンス図の処理を実装します。ゲームが起動して追加でデータをダウンロードする必要がある場合を想定しています。

図 1: サンプルアプリのシーケンス図

図 1: サンプルアプリのシーケンス図

 

  1. ゲームサーバー、もしくはアセットサーバーから最新データの情報を取得します。ここではデータ本体ではなく、データのタイムスタンプ、バージョン、URLなどだけ取得します。

  2. サーバーは最新データの情報を返します。

  3. サーバー上の最新データがローカルのものよりも新しい場合、1で取得したURLにアクセスしデータをダウンロードします。

  4. サーバーは最新データの本体を返します。なお、データのサイズは大きいものと想定します。この部分はアプリがバックグラウンドに入っても継続実行できるのが、今回の実装の特徴です。

  5. ダウンロードが完了し、アプリがバックグラウンドにいる場合、ローカル通知を利用しユーザに通知します。(ローカル通知はサンプルでの実装ですので、オプショナルです)

  6. ダウンロードしたデータのチェックサムを確認します。

  7. ダウンロードしたデータを展開します。

  8. データ処理が完了し、ゲームが開始します。

 

サンプルは、こちらで公開されていますので、実装の詳細はサンプルを参考にして下さい。ここでは3〜5の部分だけを説明します。

なお、チェックサムの検証やデータの展開はアプリによって仕組みが異なりますので、サンプルではこの部分の実装を省略しております。単純に、アプリをsleepするように仮実装しました。

 

登場クラスの紹介

サンプルには下記3つのクラスがあります。まずはそれらの間での関係について簡単に紹介します。

AppDelegate
iOSアプリでは定番のクラス。サンプルではアプリがバックグラウンドにいる際にダウンロードが完了した場合、OSによって起こされてローカル通知を送信します。

ViewController
サンプルはシングルビューアプリとして作成しました。ゲームデータのダウンロード中に進捗を表示するローディング画面と理解していただけたらいいと思います。下記のSimpleAssetManagerからデータダウンロードと処理の進捗状況を受け取って表示します。

SimpleAssetManager
今回の主役となります。ゲームデータのダウンロードと検証および展開処理の全てを管理し行うクラスです。NSURLSessionとNSURLSessionDownloadTaskに関連する実装は全てこちらのクラスにまとめて実装されています。また、SimpleAssetManagerDelegateというプロトコルを用意してdelegate(ここではViewController)に進捗を通知します。

 

ダウンロードの開始

NSURLSessionで開始されたダウンロードタスクとアップロードタスクをバックグラウンドで実行されるようにするためには、下記のようにセッションの生成時にNSURLSessionConfigurationのbackgroundSessionConfigurationメソッドで取得した設定を指定するだけです。

// create session for downloading app data
NSURLSessionConfiguration *configuration =
  [NSURLSessionConfiguration
   backgroundSessionConfiguration:
   @"com.mobage.sample.BackgroundDownloadSample.session"];
self.session = [NSURLSession 
                sessionWithConfiguration:configuration
                delegate:self delegateQueue:nil];

次に、ダウンロードを開始するためには、ダウンロードタスクを取得しresumeメソッドを呼び出します。なお、assetURLはダウンロード対象ファイルのURLです。

// kick start download
// download continues in the background after this point
NSURL *assetURL = [NSURL URLWithString:[_latestAsset objectForKey:kAssetURL]];
self.downloadTask = [self.session downloadTaskWithURL:assetURL];
[_downloadTask resume];

これだけで、データはバックグラウンドでダウンロードされますが、実用化するためには下記の実装が必要です。

 

必要なdelegateメソッド

ここでは概要だけを説明し、実装を省略させていただきます。実装例はサンプルを参考にしてください。

 

AppDelegateには、下記のdelegateメソッドの実装が必要です。

UIApplicationDelegate

- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
  completionHandler:(void (^)())completionHandler;

アプリがバックグラウンドに入り、ダウンロードが完了しますとiOSがこのメソッドを通じてアプリを起こします。ここで渡されるcompletionHandlerはどこかに保持しておく必要があります。全ての処理が終わったらこのcompletionHandlerを呼び出すと、iOSはアプリのスクリーンショットを更新します。

なお、ダウンロード完了時にアプリがフォアグラウンドにいる場合は呼び出されません。サンプルではこれを利用しここでダウンロード完了のローカル通知を表示します。

 

一方で、SimpleAssetManagerには、下記の3つのプロトコルを実装しています。

NSURLSessionDownloadDelegate

- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location;

ダウンロードが完了した際に呼び出されます。このメソッドでファイルをアプリ配下のフォルダにコピーしなければなりません。ファイルのコピー先については、こちらのApp Store規約に注意して下さい。

- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;

ダウンロードの進捗はこちらで通知されます。

- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes;

NSURLSessionDownloadDelegateの必須delegateメソッドですが、今回は利用しません。

 

NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error;

ダウンロードが完了した場合こちらで通知されます。エラーが発生した場合はerror引数にセットされますので確認が必要です。

 

NSURLSessionDelegate

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;

アプリがバックグラウンドにいる場合のみ呼び出されます。URLセッションの処理が全て終了した際に呼び出されます。ここでAppDelegateで保持しておいたcompletionHandlerを呼び出しています。

動作確認

実際の動作の様子を下記ビデオでご覧下さい。

 

課題

  • ファイルチェックサム検証や展開処理の後処理の部分にmainスレッドを利用しますと、ダウンロード完了→後処理完了の間にユーザがアプリを起動した場合、UI更新ができないため後処理の部分はmainスレッドを避けてglobalキューを利用するように実装しました。そのため、後処理の部分はバックグラウンドで実行することはできませんでした。

  • ダウンロードがバックグラウンドで実行することで待たせる感は軽減できるものの、ダウンロードに時間がかかることに変わりはありません。

  • iOSネイティブ実装の例となっていますので、UnityやCocos2d-xなどご利用の場合はこのまま利用できないかもしれません。

  • 当たり前ですが、Androidでは別途実装が必要です。

以上となりますが、次は上記課題のどれかを検証し、解決方法を提案したいと思っておりますので引き続きMobage Developers Blogを宜しくお願いします!