新機能 Background Fetch とは?

 iOS7 では UI が全面的に刷新されました。こちらに目を奪われがちですが、同じく iOS7 からサポートされるマルチタスク機能も非常に強力です。「あれ?マルチタスクは iOS4 から既に使えるんじゃ?」と思った方も多いかもしれませんが、今回のマルチタスクは一味違います。

 従来までは位置情報の取得や音楽再生などの一部の例外を除き、同時に実行できるのは現在アクティブであるアプリ 1 つのみでした。 しかし iOS7 から導入された Background Fetch を利用すると、アプリがバックグラウンドに移行しても定期的に任意の処理を実行させることができます。 これにより、例えば従来は Remote Notification を利用して「新着の検知と通知」を行っていたところを、アプリのみで実現することができます。

 それでは早速この Background Fetch を利用してみましょう。

下準備

 Background Fetch の利用を開始するには、次の 2 つを準備する必要があります。

Info.plist の設定

 Project Navigator で Xcode プロジェクトを選択し、Info タブをクリックします。 キー名が Required background modes で型が Array のプロパティを追加し、ここに値が “App downloads content from the network” の要素を追加します。

Background Fetch 有効化の設定

Info.plist ファイル内では Required background modes は “UIBackgroundModes”、 App downloads content from the network は “fetch” と記述します。

 この指定がされていないと、バックグラウンド実行が呼び出されないので注意して下さい。

setMinimumBackgroundFetchInterval: の呼び出し

 もう 1 つ、アプリの起動時に UIApplication の setMinimumBackgroundFetchInterval: メソッドを呼び出し、バックグラウンド実行の要求をします。

[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

 これでバックグラウンド実行の準備は完了です。 ちなみに引数で渡している UIApplicationBackgroundFetchIntervalMinimum は、その名の通り「最小間隔」を指定するための定数です。 実際にこの値を指定すると、約 20 〜 30 分に 1 回の割合ででバックグラウンド処理が呼び出されました。 これ以外の値を指定しても一応は受け付けてくれるようですが、正確に動く保証がないのでここはひとまず UIApplicationBackgroundFetchIntervalMinimum を指定するのが定石だと思っていいでしょう。

バックグラウンド処理の開始と停止

 さて上記の下準備を終えると、あとは OS が定期的に処理を呼び出してくれます。 実際の処理は UIApplicationDelegate の以下のメソッドが呼び出される形になります。

// バックグラウンド実行の際に呼び出される
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler
{
    // ここにバックグラウンド処理
}

 該当のアプリの ApplicationDelegate クラスに、上記の application:performFetchWithCompletionHandler: を実装すれば、自動でそれが定期的に呼び出されます。 後はこのメソッドの中に任意の処理を書けば OK です。

 ところで、ここで引数で渡る completionHandler って何でしょう? この blocks はバックグラウンド処理が完了した場合に呼び出す必要があり、OS 側に「完了したこと」を通知するためにあります。 バックグラウンド処理は通信などの非同期の処理も行えるため、メソッドが抜けた時点では処理が完了しているとは限りません。 そのためアプリ側でバックグラウンド処理が完了した際に、責任をもってこの blocks を呼び出す必要があります。

 この完了通知を呼び出す際に渡す引数ですが、それぞれ次のような意味があります。

引数の値 意味
UIBackgroundFetchResultNewData 新着データがあり、アプリ側に更新が生じた
UIBackgroundFetchResultNoData 新着データはなく、アプリ側の更新は無し
UIBackgroundFetchResultFailed バックグラウンド処理に失敗

 ちなみに UIBackgroundFetchResultNewData を設定すると、実行中のアプリ一覧に表示されるアプリのスクリーンショットが更新されます。

 またもしバックグラウンド処理の呼び出しを停止したい場合は、次のように指定します。

[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];

バックグラウンドで通信ができる NSURLSession

 ここまでで、バックグラウンド状態でも定期的に処理をできる準備が整いました。 次は実際にバックグラウンド処理が呼び出された際に、通信処理を行ってみましょう。

 通信処理といえばお馴染み NSURLConnectionですが、残念ながらこれはバックグラウンド状態では動作しません。 でもご安心を。iOS7 からはバックグラウンド状態でも通信が行える API が追加されました。 その 1 つである NSURLSession クラスですが、これは「通信処理のキュー」でしかありません。 これに「タスク」を追加することで、通信処理の登録ができます。NSURLSession に登録されたタスクは逐次処理が開始されます。

NSURLSessionの作成とタスクの追加

 NSURLSession の作成は、次のように行います。

// バックグラウンド実行させたい場合は backgroundSessionConfiguration: で作ること!
NSString *identifier = @"XXXX";
NSURLSessionConfiguration *configration = [NSURLSessionConfiguration backgroundSessionConfiguration:identifier];

// 上で作ったNSURLSessionConfigurationを指定して NSURLSession作成
NSURLSession *session = [NSURLSession sessionWithConfiguration:configration delegate:self delegateQueue:nil];

 はじめに NSURLSessionConfiguration を作る必要があります。これは読んで字の如く、これから作るNSURLSession の設定を表すクラスです。 バックグラウンド通信をさせたい場合は必ず backgroundSessionConfiguration: メソッドで生成します。

 後は今作った NSURLSessionConfiguration を指定して、NSURLSession のインスタンスを作成します。 先程「NSURLSessionは 通信処理キューである」と言った通り、このインスタンスは毎回作るというよりもアプリで 1 個持つ、といった使い方になりますので、生成したインスタンスは大事にとっておいて下さい。
 インスタンスを作成する時に指定する delegate にはいくつかの種類があります。ここで指定した種類によって、後に説明する「タスク」を使用した時に、使用可能なデリゲートのメソッドが変化します。そのため、NSURLSessionを使用するクラスのヘッダファイルで、使用する delegate を宣言しておく必要があります。これには次のような3種類が存在します。

  • NSURLSessionTaskDelegate
  • NSURLSessionDownloadDelegate
  • NSURLSessionDataDelegate

 NSURLSessionTaskDelegate は、全てのタスクで使用する基礎的なデリゲートメソッドを実装してある基底のクラスです。残り2種類の delegate クラスも、このクラスを継承しています。  NSURLSessionDownloadDelegate はダウンロードしたデータやダウンロード進捗状況の監視などの処理を行うためのメソッドを実装しているクラスです。  NSURLSessionDataDelegate は、ダウンロード開始の告知や、ダウンロードしたレスポンスヘッダやレスポンスボディなどを処理するためのメソッドを実装しているクラスです。

 実際に通信処理を行う場合は、次のように「タスクを追加する」必要があります。

// リクエスト
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://XXXX"]];

// 先に作成したNSURLSessionに対して downloadTaskWithRequest: メソッドを呼び出す
NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithRequest:request];

// resumeをしないと処理開始しない!
[downloadTask resume];

 タスクの生成と追加は NSURLSessionに 対して downloadTaskWithRequest: メソッドを呼び出します。 戻り値は NSURLSessionDownloadTask となります。

 実は、タスクには次の 3 種類存在します。

  • NSURLSessionDataTask
  • NSURLSessionDownloadTask
  • NSURLSessionUploadTask

 今回使用した NSURLSessionDownloadTask はデータのダウンロードと、ダウンロードした内容を一時ファイルとして保存する仕組みが提供されますので、大きなデータのダウンロードに適します。 NSURLSessionUploadTask は NSData かファイルパスを指定してのアップロード機能を提供します。 NSURLSessionDataTask はレスポンスを NSData で取れるので、API 通信や小さな画像をロードする際に利用できると思います。ただし、DataTaskはバックグラウンド処理で使用することはできないので注意が必要です。

 ちなみにタスクの生成・追加の直後に、それの resume メソッドを呼び出していますが、これを行わないと処理が開始されないので忘れずに呼び出して下さい。

通信結果はどうやって取る?

 通信結果の取得も、非常に簡単に取れるようになっています。 先ほどSessionを作成した際にdelegateを指定したと思います。通信の結果はその指定したdelegateのメソッドに送られます。 今回使用しているNSURLSessionDoenloadDelegateでは、以下のようなメソッドが存在します。

  • URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:
  • URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
  • URLSession:downloadTask:didFinishDownloadingToURL:

 これらは全て必須のメソッドになっているので、使用する際には実装しておきましょう。

-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
NSData *data = [NSData dataWithContentsOfURL:location];    
...
}

 URLSession:downloadTask:didFinishDownloadingToURL: はダウンロードが完了した際に呼ばれます。上記のように、locationに結果として一時ファイルのURLが渡されるのでそれをNSData等で受け取ることができます。  URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite: ではダウンロードの進捗状況を知ることができます。  URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes: ではダウンロード処理が再開された時に通知されます。  

更新情報の通知

 このようにバックグラウンドで通信が可能であることから、冒頭でもお話しした「Remote Notification を用いない通知」が可能になります。 具体的にはデータの更新を検出した際に UILocationNotification を作り、UIApplication の presentLocalNotificationNow: でこれを表示してあげます。

-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{    
   //一部省略…

    notification.fireDate = [NSDate date];    // すぐに通知
    notification.timeZone = [NSTimeZone defaultTimeZone];
    notification.alertBody = @"通知メッセージ…";
    notification.alertAction = @"Open";       // 通知メッセージタップ時の動作
    notification.soundName = UILocalNotificationDefaultSoundName;

   [[UIApplication sharedApplication] presentLocalNotificationNow:notification];
}

ここでは、先ほども使用したデリゲートメソッドの中で「Remote Notification を用いない通知」を実装してみました。  ただ、この仕組みでは Remote Notification を完全に置き換えられるわけではありません。 その理由は次章を読んでいただければ理解いただけると思います。

Background Fetch の注意点

 一見有用な Background Fetch も、いくつか注意しなければならない点があります。

必ずしも一定間隔でコールバックされるわけではない

 今のところ UIApplication の setMinimumBackgroundFetchInterval: で指定した値は、正確な時間として解釈していないようです。  筆者の検証では 20 〜 30 分間隔でしたが、デバイスや OS バージョンによって異なることも考えられますので、前提ありきの実装は避けるべきでしょう。

デバイスがスリープ状態でもコールバックされることがある

 タスクを処理する以上、その間は電池を消耗します。 コールバック処理は極力負荷をかけないよう、注意して実装する必要があります。

アプリのプロセスを終了させてしまうとコールバックされない

 Background Fetch はアプリのプロセスが生きている間のみ、有効です。 アプリはユーザの操作によって終了させることが可能ですので、前章で紹介した「Remote Notification を用いない通知」が機能しなくなることがあります。

おわりに

 第 1 回目の特集はいかがだったでしょうか。 使い方に注意はあるものの、Background Fetch は iOS アプリにまた新たな可能性を与える機能の 1 つであると感じていただけたのではないかと思います。

 今回紹介はできませんでしたが、Background Fetch には例えば「バックグラウンド処理の進捗を確認する」機能など、より細かにコントロールする方法も提供されています。 さらに進んだ使い方については、また別の機会に紹介できればと考えております。

次回の特集もお楽しみに!