Remote Push からの Background Fetch を試してみた

前回は Background Fetch を試してみました。この方法ではアプリだけの実装でバックグラウンドで通信処理等を走らせる事ができますが、実行タイミングが OS 側の学習によるもので任意のタイミングでは発動させることができません。iOS 7 からは Background Fetch 以外にも Remote Push Notification をトリガーに Background Fetch を実行させる機能も追加されています。Remote Push Notification を送るためのサーバサイドの準備が必要ですが、任意のタイミングで実行できる(Rate Limit はあります)のは魅力的です。APNs 環境を作ったことがない僕が APNS 環境の構築から Remote Push Notification から Background Fetch を動かすまでのメモを残しておきます。

APNs 環境構築

今回、僕が使ったのは Heroku + Helios という環境を構築してみました。Heroku は個人開発者が動作検証に使う分には十分に無料の範囲でいろいろとためせて非常に助かります。Helios は AFNetworking の作者であり、Heroku 社員でもある Mattt が作った iOS デベロッパ向けの各種バックエンド機能を実装した Framework です。Heroku 社員でもあるだけに Heroku へのデプロイも簡単です。

まずは、Helios のインストール

gem install helios

次に Helios のアプリケーションを作成

helios new <任意のアプリケーション名>

これだけで必要なファイル群が作成され、git の管轄下になります。

今の Heroku は Ruby 2.0.0 で動いているので Gemfile に以下を追記。

ruby '2.0.0'

APNs に必要な pem ファイルを作成します。このあたりはあらゆるところに情報があるので割愛します。

作成した pem ファイルをディレクトリに追加。

pem ファイルを読み込み、APNs が使えるようにするための設定を config.ru ファイルに追記

service :push_notification, apn_certificate: './apple_push_notification.pem', apn_environment: 'development'

ここまでで下準備が大分整いました。あとは heroku 上にアプリケーションを作成します。 Heroku の基本的なセットアップは事前に終わっていることとします。

今回作成した Helios アプリの git リポジトリディレクトリ内で

heroku create

すると Heroku 上に環境を作ってくれます。このときに URL も出力されるので覚えておいた方があとあと便利です。ここまできたらあとは、

git push heroku master

すれば Heroku 上へのデプロイも完了です。

アプリへの組み込み

Bundle ID の修正

普段の開発ではプロビジョニングプロファイルをワイルドカード OK な設定にしてサンプルアプリを作っているときにはあまり意識していないかも知れませんが APNs では Bundle ID が合致している必要があります。まずは Bundle ID をそろえておきます。プロビジョニングプロファイルもAPNs が受信出来るものをセットします。

APNs を使うためのコード

何はなくとも APNs を使うためには Apple のサーバに登録しないといけません。application:didFinishLaunchingWithOptions: 内あたりに

[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeNewsstandContentAvailability | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];

を書きます。個人的に試してみた感想では UIRemoteNotificationTypeNewsstandContentAvailability を宣言しておいた方が Remote Push Notification での Background Fetch が動きやすい気がしています。特にドキュメントに言及されていないように思えますので真偽のほどはわかりません。

Apple の APNs サーバーに対して登録を行ったあとは自身が用意する APNs サーバー側に取得したトークンを登録しておく必要があります。今回は Heroku 上にセットアップした Helios に対してこの作業を行います。今回は Helios を作った Mattt 作の ライブラリ Orbiter を使ってみます。Orbiter は AFNetworking ベースで作られた APNs 登録処理の補助ライブラリです。

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    NSURL *serverURL = [NSURL URLWithString:@"http://foobarbuzz.herokuapp.com/push_notification"];
    Orbiter *orbiter = [[Orbiter alloc] initWithBaseURL:serverURL credential:nil];
    [orbiter registerDeviceToken:deviceToken withAlias:nil success:^(id responseObject) {
        NSLog(@"Registration Success: %@", responseObject);
    } failure:^(NSError *error) {
        NSLog(@"Registration Error: %@", error);
    }];
}

上記コードを書くだけでとりあえず動きます。個人的にハマったのは URL です。Orbiter のサンプルだと Heroku 上に作成したアプリの URL を書いていますが、これではダメでした。ググったりして調べてみると Helios 側のソースコードを除いてみたところ rack/push-notification に丸投げしてるようです。ということで http://foobarbuzz.herokuapp.com ではなく http://foobarbuzz.herokuapp.com/push_notification を指定します。Helios の github の README にも書いてありました。

これでアプリを起動して、orbiter の Registration Success まででれば OK。

意気揚々とビルドして

“aps-environment”エンタイトルメント文字列が見つかりません

とか言われたら正しいプロビジョニングプロファイルをセットしていない可能性があるので確認しましょう。僕は2回もやらかしました。

実際に Push を送る

Helios の README には curl を使った例があるのですが、Remote Push Notification による Background Fetch を実行するために必要な "content-available" を送っても Helios 側が受け取れません。ソースコードを読んでみるとそもそも "content-available" には対応してなさそうです。ということでまたまた Mattt 謹製のライブラリ Houston を使って Push を送ります。

houston のインストールは

gem install houston

もしくは他の関連コマンドもまとめてインストールできる nomad を入れても良いかもしれません。

gem install nomad-cli

インストールができたら普通の Push を送ってみます。

apn push DEVICE_TOKE -c PATH_TO_PEM_FILE -m "Test Push"

DEVICE_TOKE は APNs に登録した際に取得したトークンです。Helios 側にも登録したはずなので、http://foobarbuzz.herokuapp.com/admin/#push-notification のように自身の Heroku アプリの URL にアクセスすることでもわかります。 PATH_TO_PEM_FILE は Heroku にデプロイした pem ファイルへのパスです。 -m オプションは Push に乗せるメッセージです。

これで実機で受信できれば OK です。

Remote Push から Background Fetch

前回と同様に Xcode 5 の Capabilities を開いて、Remote notifications にチェックをつけておきます。

次にメソッドの実装。これは前回と似たようなメソッド - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler を実装します。

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    if(![userInfo[@"aps"][@"content-available"] intValue])
    {
        completionHandler(UIBackgroundFetchResultNoData);
        return;
    }
   
    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 RemoteNotification Fetch"
                                               , [dateFormatter stringFromDate:fromDate]
                                               , [dateFormatter stringFromDate:toDate]
                                               , [@(numberOfSteps) stringValue]];
                             
                             UILocalNotification *localNotif = [[UILocalNotification alloc] init];
                             localNotif.timeZone = [NSTimeZone defaultTimeZone];
                             localNotif.alertBody = text;
                             [[UIApplication sharedApplication] presentLocalNotificationNow:localNotif];
                             
                             [self updateBadgeCountWithCurrentDate:toDate];
                             
                             return text;
                         } completionBlock:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                             if (error) {
                                 UILocalNotification *localNotif = [[UILocalNotification alloc] init];
                                 localNotif.timeZone = [NSTimeZone defaultTimeZone];
                                 localNotif.alertBody = [error description];
                                 [[UIApplication sharedApplication] presentLocalNotificationNow:localNotif];

                                 completionHandler(UIBackgroundFetchResultFailed);
                             } else {
                                 completionHandler(UIBackgroundFetchResultNewData);
                             }
                         }];
}

"content-available" をチェックしているのは Fetch 処理が必要ない場合に呼ばれたときに余計な通信をしないためにしています。本当はもう少しいろんなシチュエーションに応じて分岐処理が必要かも知れません。

Push から発動させるには apn コマンドに -n オプションをつけておきます。

apn push DEVICE_TOKE -c PATH_TO_PEM_FILE -n -m "Test Push"

Silent Push

次に Silent Push を送ってみます。Silent Push とは Payload にメッセージを含めず "content-available" のみを送るものです。この Silent Push の場合 Remote Push Notification による Background Fetch は発動しますが、通常の Push のように Push そのものは Notofication Center に表示されないことから Silent と呼ばれています。実装によっては、Silent Push を受け取り Background Fetch で動作が完了した際に Local Push を実行するということも出来ます。このような実装をした場合にはエンドユーザーに取っては Notification Center の通知が来たと同時にアプリを開けばその内容に応じたコンテンツがすでに取得済みということが出来ます。

Silent Push の送り方ですが、ここでも難点が。私の環境では

apn push DEVICE_TOKE -c PATH_TO_PEM_FILE -n

とするとエディタが開いてしまいます。

apn push DEVICE_TOKE -c PATH_TO_PEM_FILE -P '{"aps": {"content-available":1}}'

とするか、Houston のサイトにあるようにスクリプトとして実装してしまう方法をとりました。

私の環境では Silent Push を基点に Background Fetch を動かすのは Rate Limit に引っかかっているせいなのか多少間をあけないと実行できませんでした。

トラブルシューティング的ななにか

  • push を登録するときには UIRemoteNotificationTypeNewsstandContentAvailability を宣言しておいた方が Remote Push Notification での Background Fetch が動きやすい気がする
  • うまく Push が発動しない時には unregister してみる
  • いきなり込み入ったことをせずにシンプルにバッジの更新程度で試してみる

感想

期待通りに動かない時があったりしたので、実際にタイムリーに発動させるためにはもう少し研究が必要な気がしますが期待度の高い機能なので取り組んでみる価値は高いと思いました。