本記事ではアプリ側のRemote Notification(Push通知)における実装について解説したいと思います。Push通知を実装する準備段階で、いろいろやることがありますが、これについては既に多くの解説記事がありますので、そちらを参照してください。

iOS7での話ですので、それ以降のバージョンでは挙動が異なるかもしれませんので、ご注意くださいませ。

本記事はPush通知できるようになった状態からの実装の話、つまり実践的な内容にしたいと思います。説明する前に混乱を避けるため言葉の定義をはっきりさせておきましょう。

Push通知 本記事ではサーバーからのPush通知のみを指す。正確にはRemote Notificationと呼ぶが、本記事では便宜上Push通知と呼ぶ
起動 アプリが起動している状態。フォアグラウンドかバックグラウンドかは問わない
フォアグラウンド アプリが前面にあり画面に表示されている状態
バックグラウンド アプリが背面にありホーム画面、もしくは他のアプリが画面に表示されている状態
Active フォアグラウンドかつアプリの操作などが出来る状態
Inactive アプリの操作などができない状態。バックグラウンドからフォアグラウンドへ復帰した直後や、通知センターやコントロールセンターがアプリを覆って表示されている時、この状態になる

まずはアプリがPush通知をどのように検出できるかについて説明します。

Push通知からアプリをフォアグラウンドにしたことを検出する

Push通知からアプリがフォアグラウンドにすることがあります。アプリが起動してない場合は、起動してからフォアグラウンドになります。

アプリが未起動だった場合は、まずはおなじみの以下のメソッドが呼ばれ、launchOptions引数からPush通知の内容を取得できます。

- (BOOL)application:(UIApplication *)application 
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // もろもろの処理

    NSDictionary *userInfo = 
      [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
    if (userInfo) {
        NSDictionary *aps = [userInfo objectForKey:@"aps"];
        NSString *alertMessage = [aps objectForKey:@"alert"];
    }

    // もろもろの処理
    
    return YES;
}

アプリが起動、未起動問わず、以下のメソッドが呼ばれます。userInfo引数からPush通知の内容を取得できます。

- (void)application:(UIApplication *)application 
    didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    NSDictionary *aps = [userInfo objectForKey:@"aps"];
    NSString *alertMessage = [aps objectForKey:@"alert"];
}

Push通知経由でアプリをフォアグラウンドにしようとしたとき以下の順でメソッドが呼ばれます。

  • application:didLaunchingWithOptions(未起動から起動したときのみ呼ばれる)
  • application:didReceiveRemoteNotification(Inactiveであることに注意)
  • applicationDidBecomeActive

application:didLaunchingWithOptionsの方でPush通知処理することは、あまりないかもしれません。必ず呼ばれるapplication:didReceiveRemoteNotificationだけ実装で、ほとんどの場合は事足りると重います。

注意点

Push通知からアプリをフォアグラウンドにしようとした時、application:didReceiveRemoteNotificationでは、Inactiveであるということに注意が必要な場合があるかもしれません。

また、アプリがフォアグラウンドのときに通知センターを表示(上端から下へ指を動かすと表示される)した時、アプリはInactiveになることにも注意が必要です(バックグラウンドにはなりません)この時、通知センターからPush通知をタップすると、アプリは上記順番でメソッドが呼ばれてActiveになります。

ちなみにActive,Inactive,Backgroundかどうかは、UIApplication#applicationStateで取得できます。判別が必要な時はこのメソッドを使いましょう。

端末にPush通知が届いた瞬間を検出する

アプリによってはPush通知が届いた瞬間に何かをしたいということがあるかもしれません。

フォアグラウンドでのみ検出できれば良い場合

この場合は、application:didReceiveRemoteNotificationを実装すればよいです。フォアグラウンドの時にPush通知が届くと、このメソッドが呼ばれるからです。ちなみにこのときのapplicationStateは当然Activeです。

バックグラウンドでも検出したい場合

バックグラウンドでもPush通知が届いた瞬間に何かしたいことがあるかもしれません。ユーザーに何もメッセージ表示しないPush通知を送ることもできます。このような通知をSilent Remote Notificationと呼びます。たとえばアプリがバックグラウンドにあるときにSilent Remote Notificationを受け取り、バックグラウンドでダウンロード処理をするような実装ができます。

バックグラウンドでも検出したい場合、以下のようにBackground ModeのRemote NotificationをONにする必要があります。

Push01

バックグラウンドモードを有効にした時は、application:didReceiveRemoteNotification:の代わりにapplication:didReceiveRemoteNotification:fetchCompletionHandler:を実装するようにします。注意すべきなのは、このメソッドを実装した時、application:didReceiveRemoteNotificationは呼ばれなくなることです。application:didReceiveRemoteNotification:fetchCompletionHandler:はバックグラウンドでPush通知を受け取った時だけ呼ばれるのではありません。application:didReceiveRemoteNotification:が呼ばれるタイミングで、代わりに呼ばれるようになります。

- (void)application:(UIApplication *)application 
  didReceiveRemoteNotification:(NSDictionary *)userInfo 
  fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    if (userInfo) {
        NSDictionary *aps = [userInfo objectForKey:@"aps"];
        NSString *PushMessage = [aps objectForKey:@"alert"];
        
        UIApplicationState appState = application.applicationState;
        NSString *appStateString = @"unknown";
        if (appState == UIApplicationStateActive) {
            appStateString = @"active";
        } else if (appState == UIApplicationStateInactive) {
            appStateString = @"inactive";
        } else if (appState == UIApplicationStateBackground) {
            appStateString = @"background";
        }
        NSLog(@"Receive remote notification. State:%@", appStateString);
    }

    // completionHandlerはダウンロードのような時間がかかる処理では非同期に呼ぶ。
    // 同期処理でも呼ばないとログにWarning出力されるので注意。
    completionHandler(UIBackgroundFetchResultNoData);
}

さらにサーバー側で以下のようにcontent-availableを1にしてPUSH送信する必要があります。

"aps":{
    "alert": "Are you OK?",
    "content-available": 1
  }

アプリが起動していない時は、Push通知が届いたことを検出できない

バックグラウンドモードにするとフォアグラウンド、バックグラウンドでPush通知が届いたことを検出できます。しかし、これはアプリが起動していることが前提となります。アプリが起動していない時は、リアルタイムなPush通知検出はできなさそうです。

SDKのドキュメントには以下のように書かれています。

When a Push notification arrives, the system displays the notification to the user 
and launches the app in the background (if needed) so that it can call this method.

この説明によると、

  • アプリが未起動の時にそのアプリのPush通知を受け取る
  • アプリが起動しバックグラウンド状態のままになる
  • application:didReceiveRemoteNotification:fetchCompletionHandler:が呼ばれる

という動作になるように思えます。しかし、実際はそのような挙動にはなりません。アプリは未起動のままです。

このことは他のブログやStackoveflowなどでも話題となっています。こちらの記事を書かれた方が、Appleに直接問い合わせたところ、問い合わせがたくさんありドキュメントを整理中とのことです・・・

[iOS 7] Remote notificationの挙動について

補足

  • アプリを起動したまま、端末の電源をオフにする
  • 端末の電源をオンにする
  • ホームボタンを2回押す
  • 起動していたアプリが一覧表示される

このとき、あたかもアプリが起動しているように見えますが、実際は起動していません。起動していたもののスクリーンショットで一覧表示しているだけです。つまり、この状態でもPush通知が届いたことを検出することはできません。

この記事はiPhone 5 + iOS7での調査です。ブログを読みあさるとiPhone 5Sでは、起動状態であるといった話もありましたが定かではありません。いずれにせよ、基本的にはPush通知は未起動の時はアプリ側で検出できない前提で実装したほうが良いでしょう。