Background Fetch を試してみた

先日、「M7 と少しだけ戯れてみた」というエントリでモーションアクティビティを試しに触ってました。その流れで自分の歩数を定期的に Tweet するようにしてみたらおもしろいかなぁと思って折角なので iOS 7 から導入された Background Fetch を使って見ようと思い、その時の内容をメモとして残してみようと思います。

ここに書く実験内容は Apple の審査を通った実績のあるものではない点をご了承ください。

Background Fetch とは

  • iOS 7 から追加された新しい Background Mode のひとつ
  • OS 側が不定期(OS の判断で適切だと思われるタイミング)で - (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler メソッドを Background で呼び出してくれる
  • 実行される最短インターバルの指定が setMinimumBackgroundFetchInterval: メソッドで可能
  • OS からの呼び出しタイミングはアプリの利用状況(パターン)を学習した上で決定される

というもののようです。

feedtailor さんの "そら気温" でも使われています。

Background Fetch でできること

Apple の紹介している例としては、

  • SNS 系アプリのタイムラインの事前取得
  • News 系アプリの新着情報事前取得
  • 天気系アプリの情報取得(このあたりが feedtailor さんの "そら気温" がやっているところ)
  • 写真や動画の共有(別の技術 "Background Transfer" との併用)

があげられています。

具体的には

  • Network 通信
  • アプリケーションバッジの更新
  • Local Notification の発行
  • ローカルファイルの生成(UserDafaults の書き換えやおそらく DB 系操作も)

などが可能です。

ということで、冒頭でも書いたように Background Fetch が発火したタイミングで現在の歩数累計を Background Fetch で Tweet してました。

実装

必要なの作業は以下の通りです。

  1. Bacground Modes を "Fetch" として Info.plist に記述
  2. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptionssetMinimumBackgroundFetchInterval: を設定
  3. - (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler の中身を実装

1. Bacground Modes を "Fetch" として Info.plist に記述

以下のスクリーンショットのように Xcode 5 上で設定

f:id:griffin-stewie:20130929183626p:plain

2. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptionssetMinimumBackgroundFetchInterval: を設定

application:didFinishLaunchingWithOptions: 内で setMinimumBackgroundFetchInterval: を呼びます。ここでは定数の UIApplicationBackgroundFetchIntervalMinimum をセットしています。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
    return YES;
}

3. - (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler の中身を実装

- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    StepCountTweeter *tweeter = [[StepCountTweeter alloc] init];
    [tweeter tweetStepCountWithUserName:nil
                         tweetTextBlock:^NSString *(NSInteger numberOfSteps, NSDate *fromDate, NSDate *toDate, NSError *error) {
                             NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
                             [dateFormatter setLocale:[NSLocale systemLocale]];
                             [dateFormatter setDateFormat:@"MM/dd HH:mm:ss"];
                             NSString *text = [NSString stringWithFormat:@"%@ 〜 %@ の期間に iPhone 5s を持って %@ 歩 歩きました。 Tweet By Background Fetch"
                                               , [dateFormatter stringFromDate:fromDate]
                                               , [dateFormatter stringFromDate:toDate]
                                               , [@(numberOfSteps) stringValue]];
                             return text;
                         } completionBlock:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                             if (error) {
                                 /// 失敗時には UIBackgroundFetchResultFailed を渡して completionHandler を呼ぶ
                                 completionHandler(UIBackgroundFetchResultFailed);
                             } else {
                                 /// 成功時には UIBackgroundFetchResultNewData を渡して completionHandler を呼ぶ
                                 completionHandler(UIBackgroundFetchResultNewData);
                             }
                         }];
}

- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler の中で具体的に Background Fetch として実現したいことを実装します。約 30 秒の猶予が与えられます。 StepCountTweeter クラスは今回歩数を Tweet するために僕が作ったクラスで SLRequest を使って Tweet するクラスです。Background Fetch でのポイントは StepCountTweeter クラスの tweetStepCountWithUserName:tweetTextBlock:completionBlock: メソッドの第3引数の completionBlock 内部で - (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler の completionHandler を呼んでいるところです。この completionHandler は必ず呼ぶ必要があります。

Tips

Background Fetch の有効/無効

アプリによっては、Background Fetch 自体は有効にしたいもの ユーザー設定として Background Fetch での動作を On/Off させたいとかそもそも Background Fetch で実行したい処理でログインが必須などの場合があるかと思います。アプリ側の状況で実際に Background Fetch の細かい On/Off が必要な場合は以下のように setMinimumBackgroundFetchInterval: に渡す値で有効 OR 無効の切り替えが行えます。

// 無効にする
[application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];

// 有効にする
[application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum]; // もしくは任意の NSTimeInterval 

Background Fetch のデバッグ方法

Background Fetch のデバッグ方法は主に 2 つあります。

1つ目はスキームを分けてビルドのオプションでアプリ終了状態からの Background Fetch を試す方法です。

f:id:griffin-stewie:20130929183542p:plain

2つ目は Xcode の Debug メニューから "Simulate Background Fetch" を実行する方法です。この方法の場合にはアプリのプロセスが生きている間に Background Fetch が呼び出されるシチュエーションを試すことができます。

f:id:griffin-stewie:20130929183557p:plain

時間のかかる通信処理

30秒の制限がありますが、"Background Transfer" を併用することで時間のかかる通信処理でも実装可能だと思います。

実験

Background Fetch は OS によってスケジューリングされるということなのでどのくらいのスパンで実行されるのかが気になったので簡単に調べてみました。気になったのは

  • completionHandler に渡す結果によって Background Fetch の実行に影響がでるのか?
  • setMinimumBackgroundFetchInterval: に渡すインターバルの違いで起こる Background Fetch の実行への影響

です。

そこで以下のように値を調整しつつ試してみました。

  1. 取得後の結果判定を Fail にしていた インターバルは UIApplicationBackgroundFetchIntervalMinimum
  2. 取得後の結果判定を NewData に変更した インターバルは UIApplicationBackgroundFetchIntervalMinimum
  3. インターバルを 2時間に広げる

実践してみての気付き

取得結果を UIBackgroundFetchResultFailed にしても、UIBackgroundFetchResultNewData しても特に Background Fetch の実行スパンが広がったり縮まったりしているように感じなかった。

UIApplicationBackgroundFetchIntervalMinimum を指定していると

  • 最短 10 分程度
  • 最長 5 時間

くらいのインターバルで動いている感じだった。

インターバルを 2 時間に広げた場合には

  • 最短 2 時間前後( 2 時間切っている場合もあった)
  • 最長 22.5 時間

くらいのインターバルで動いている感じだった。

なんとなく、新たにビルド&インストールを行ったあとは Background Fetch が実行されるまで時間がかかるように感じた

感想

Apple の話によると Background Fetch はアプリの利用状況に応じて OS 側で呼び出してくれため、今回試してみたサンプルアプリの明示的な起動頻度等が影響しているかもしれないと思いました。アクティブに使われるようなアプリの場合は指定したインターバルに近い頻度で実行されるかもしれないですし、ほぼ起動しないようなアプリの場合は相当間が空くか最悪ほぼ実行されないとかがあるのかも知れないとも思いました。

懸念

実行タイミングを明示的には指定できないもののある程度自由度が高いことができそうです。それ故にいたずらに重い通信処理を Wi-Fi 以外の通信手段の時に行われてしまうことやログ的な情報を送信されたり、広告系の何らかのデータ処理が行われそうで1ユーザーとしては嫌だなぁと思うことがあります。開発者はお行儀の良いユーザーに喜ばれるような実装を心がけて便利で有用なものにしてもらいたいと思います。