Cordova iOS アプリ + phonegap-plugin-push でリモートプッシュ通知機能を実装するための全工程

クリスマス・イヴなので長編です。

Cordova 製の iOS アプリで、リモートプッシュ通知を受け取り処理するアプリを作ってみようと思う。Cordova に限らずリモートプッシュ通知を利用する iOS アプリ開発で必要になる手順が多いので、Cordova を使っていない iOS アプリ開発者にもご覧いただきたい。

目次

Apple Developer アカウントの用意と各種証明書発行

iOS アプリにおけるリモートプッシュ通知は、Apple Push Notification Service (APNs) を利用することが前提となる。APNs がプッシュ送信するサーバとプッシュ通知を受け取るクライアントアプリ側の間に立ち入り、プッシュ通知の内容を監視・橋渡ししている。

プッシュ通知を利用するためには、有償の Apple Developer Program に登録する必要がある。さらにその上で各種証明書の登録・設定をしておかないといけないので、結構面倒臭い。

今回はお試し実装なので、Development (開発) 環境向けの説明となる。Develop 環境で用意する要素は以下のとおり。

  1. Apple ID : Apple のアカウント。作成後、Apple Developer Program に加入し、Admin 権限を設定しておく。
  2. 認証局証明書要求 (CSR) ファイル : 証明書発行に必要な要求ファイル。Mac のキーチェーンアクセスで生成する。
  3. App ID : プッシュ通知機能を利用するアプリのバンドル ID を決めて登録する。
  4. Devices : 開発に使用する iOS 端末の UDID を登録する。
  5. iOS App Development : 2. の CSR ファイルを基に発行する、iOS アプリ開発に必要な証明書。アプリの Provisioning Profiles (7.) に取り込む。
  6. Apple Push Notification Service SSL (Sandbox) : 2. の CSR ファイルを基に発行する、プッシュ通知に必要な証明書。プッシュ通知を送信するサーバ側で利用する。
  7. Provisioning Profiles : XCode ビルド時に指定するプロビジョニングプロファイル。3. App ID・4. Devices・5. iOS App Development の情報を紐付ける。

以下、これらの要素の作成手順。当たり前だが Mac 端末が必要になる。

  1. 予め Apple Developer Program にアカウントを登録し、Admin 権限を付与しておく。
  2. Apple ID は、iPhone から紐付ける Apple ID として作成しようとすると、クレジットカード情報を入力しなくて済む。
    • Mac 上の iTunes より無料のアプリや音楽をダウンロードしようとする時に作成するとクレジットカード情報を入力しなくて済む、という情報もあったが、現在は出来なくなっているみたい。
  3. Apple Developer Enterprise Program (法人向けのプログラム) を利用する場合、Members 権限ではデバイス登録等一部の操作ができないので Admin 権限が必要。
  4. Mac の「キーチェーンアクセス」アプリより、認証局証明書要求 (CSR) ファイルを作る。
  5. 「キーチェーンアクセス」メニュー →「証明書アシスタント」→「認証局に証明書を要求」
  6. 「証明書情報」ウィザード
    • ユーザのメールアドレス : Apple Developer Program に登録したアドレスにしておく
    • 通称 : 空欄にせず、半角英数字で何らか設定する (これがキーチェーンアクセスの一覧に表示される名前になる)
    • CA のメールアドレス : 空欄
    • 「ディスクに保存」をチェックする
    • 「鍵ペア情報を指定」はチェックしない (「チェックすること」という文献もあったがチェックせず問題なかった)
  7. これで CertificateSigningRequest.certSigningRequest ファイルがローカルに保存できる。
  8. キーチェーンアクセス一覧に、通称で指定した名前の公開鍵と秘密鍵が追加される。
  9. 秘密鍵は生成した Mac 端末にのみ保存される。Mac 端末が故障した際に復元できなくなるため、パスワードをかけて書き出しておく。
    • 追加された「通称」の秘密鍵を選択し、「"通称" を書き出す」を選ぶ
    • フォーマット : 個人情報交換 (.p12)
    • 保存時に開封用のパスワードを設定する。任意のパスワードで秘密鍵ファイルを保護して保管しておく。
  10. Apple Developer ページの「Certificates Identifiers & Profiles」 より App ID を作成する。
  11. Identifiers → App IDs の右上の「+」ボタンより新規登録する。
  12. 「Explicit App ID」を選択し、バンドル ID を決める。これがプッシュ通知機能を盛り込むアプリのバンドル ID となる。プッシュ通知を利用する場合はワイルドカードではダメ。
  13. 「App Services」で「Push Notifications」を選択する。Game Center と In-App Puerchase はデフォルトで選択済み。
  14. これで App ID を作成すると、「Push Notifications」項目が「Development : Enabled」「Distribution : Configurable」 となっているはず。Distribution が Enabled になると、Development 用の証明書が利用できなくなるため、まずは Development のみ Enabled の状態であることを確認する。
  15. 「Certificates Identifiers & Profiles」より、開発に使用する iOS デバイスを登録する。
  16. Devices → Development の右上の「+」ボタンより新規登録する。
  17. 「Name (デバイス名)」は適当に分かるものにし、「UDID」に iOS デバイスの UDID を指定する。
    • 端末の UDID は、端末を USB 接続して iTunes を開き、「概要」画面の「シリアル番号」部分をクリックすると UDID 表示に切り替わるので、右クリックから「コピー」を選択して取得できる。
  18. 「Certificates Identifiers & Profiles」より、iOS アプリ開発の証明書「iOS App Development」を作成する。
  19. Certificates → Development の右上の「+」ボタンより新規登録する。
  20. iOS アプリ開発用の証明書は「Development」の「iOS App Development」を選択して「Continue」ボタンをクリックする。
  21. 「手順 2.」で作成した CSR ファイルをアップロードする。
  22. これで証明書が作成でき、ios_development.cer ファイルがダウンロードできるので、ダウンロードしてダブルクリックし、「キーチェーンアクセス」に追加しておく。
    • XCode 上で自動的に証明書が作成された場合は、「名前 苗字 (端末名)」という風に端末名入りの名前で証明書情報が登録されるので、ブラウザ上で作成した証明書と区別が付けられる。
  23. 「Certificates Identifiers & Profiles」より、プッシュ通知用の証明書「Apple Push Notification Service SSL (Sandbox)」を作成する。
  24. Certificates → Development の右上の「+」ボタンより新規登録する。
  25. 「Development」の「Apple Push Notification Service SSL (Sandbox)」を選択して「Continue」ボタンをクリックする。
  26. プッシュ通知サービスを利用するアプリのバンドル ID として、「手順 3.」で作成した App ID を指定して「Continue」ボタンをクリックする。
  27. 「手順 2.」で作成した CSR ファイルをアップロードする。
  28. これで証明書が作成でき、aps_development.cer ファイルがダウンロードできるので、ダウンロードしてダブルクリックし、「キーチェーンアクセス」に追加しておく。
  29. 以上の手順で作成できる証明書は、Identifiers → App IDs →「手順 3.」で作成した App ID → Edit ボタン → Devlopment の「Create Certificate」ボタン…と進んで作成しても同じ。
  30. 「Certificates Identifiers & Profiles」より、プロビジョニングファイルを作成する。
  31. Provisioning Profiles → Development の右上の「+」ボタンより新規登録する。
  32. 「Development」の「iOS App Development」を選択して「Continue」ボタンをクリックする。
  33. 「手順 3.」で作成した App ID を指定して「Continue」ボタンをクリックする。
  34. 「手順 5.」で作成した iOS App Development 証明書を選択して「Continue」ボタンをクリックする。
  35. 「手順 4.」で作成した Devices を選択して「Continue」ボタンをクリックする。
  36. Profile Name は分かるように付けて「Continue」ボタンをクリックする。
  37. これでプロビジョニングプロファイルが作成でき、【Profile Name】.mobileprovision ファイルがダウンロードできるので、ダウンロードしてダブルクリックし、XCode に取り込んでおく。後で XCode ビルドする際にこのプロビジョニングプロファイルを指定する。

コレでようやくアプリを作るための準備ができた。

phonegap-plugin-push プラグインの導入と XCode 設定

Cordova アプリでリモートプッシュ通知を受け取るには、phonegap-plugin-push という Cordova プラグインを利用する。。phonegap-plugin-push の利用には CocoaPods というライブラリ管理ツールをインストールする必要がある。

CocoaPods と phonegap-plugin-push の導入から XCode ビルドまでの準備は以下のとおり。

  1. $ sudo gem install cocoapodsCocoaPods をインストールする。
  2. Cordova アプリプロジェクトディレクトリ直下で $ pod setup を実行する。15分程度待つ。
  3. $ cordova plugin add phonegap-plugin-push でプラグインをインストールする。
  4. config.xml のバンドル ID を、「前章の手順 3.」で作成した App ID に変更する。
  5. アプリ実装後 (別途説明)、$ cordova build ios でビルドしたら、./platforms/ios/ 配下の .xcodeproj ファイルではなく .xcodeworkspace ファイルを選択して XCode を開く。
  6. phonegap-plugin-push が依存する CocoaPods を導入したことで、アプリと一緒に「Pods Project」も含めてビルドしないといけなくなったため、.xcodeworkspace ファイルを開く必要がある。./platforms/ios/ 配下を見ると CocoaPods 関連のファイルが追加されていることが分かる。
  7. .xcodeproj ファイルを開いて XCode ビルドすると「Apple Mach-O Linker Error」「linker command failed with exit code 1」といったエラーが出る。
  8. $ cordova run ios などのコマンドだと EXPORT FAILED (エラーコード 70) になる。エラーコード 70 については Enterprise のプロビジョニングプロファイルだと発生する場合がある、といった文献もあったが、いずれにせよコマンドラインだけでうまくビルドしきる回避策がなさそうなので、$ cordova build ios したらエラーは無視し、XCode で端末にビルド・インストールするしかなさそう。
  9. XCode を開いたら、対象のプロジェクト → TARGETS から対象のプロジェクト → General タブ → Signing → Automatically manage signing チェックを外す
  10. 同じく General タブ → Signing (Debug) → Provisioning Profile で「Import Profile」より、先程作成したプロビジョニングプロファイル (.mobileprovision ファイル) を選択する。
  11. 対象のプロジェクト → PROJECT から対象のプロジェクト → Build Settings タブ → Signing → Code Signing Identify → Debug → Any iOS SDK で、先程作成したプロビジョニングプロファイルを選択する。
  12. 「iPhone Developer: 【メールアドレス】」といった表記になっているモノ。
  13. 対象のプロジェクト → TARGETS から対象のプロジェクト → Capabilities タブで、「Push Notifications」を ON にし、「Background Modes」内の「Background Fetch」「Remote Notifications」の2つもチェックを付ける。
  14. ココまでで「Add the Push Notification feature to your App ID」といったエラーが出ている場合は XCode を再起動すると直る。

以上で XCode ビルドのための準備ができた。

phonegap-plugin-push プラグインによるクライアントアプリの実装

いよいよアプリの実装に入る。今回フロントエンドとして使用するフレームワークは特に指定しない。phonegap-plugin-push を導入したアプリ内で、以下のように実装しよう。

document.addEventListener('deviceready', () => {
  // プッシュ通知の初期処理を行う
  const push = window.PushNotification.init({
    ios: {
      alert: true,      // 通知を有効にする
      badge: true,      // バッジを有効にする
      sound: true,      // 着信音を有効にする
      clearBadge: true  // true にするとフォアグラウンドにした時にバッジをクリアする
    }
  });
  
  // プッシュ通知サービスに登録しデバイストークンを取得する
  // 初回はココでプッシュ通知の許可ダイアログが表示される
  push.on('registration', (data) => {
    // プッシュ通知が許可されるとこのコールバックが実行される
    // このデバイストークンを指定してプッシュ通知を送信するので、以下の情報をサーバに送信しておく
    console.log(data.registrationId);
  });
  
  // プッシュ通知を受信した時の処理
  push.on('notification', (data) => {
    if(data.additionalData.coldstart) {
      // ColdStart : 通知バナーのタップによってアプリが起動した場合
    }
    else if(data.additionalData.foreground) {
      // フォアグラウンド状態で通知を受信した場合
      // phonegap-plugin-push はフォアグラウンド時にバナー通知等を表示してくれない
      alert(`新着通知を受信しました\n${data.message}`);
    }
    else {
      // アプリ起動済でバックグラウンドから復帰した場合
    }
    
    // 通知の受信処理を明示的に終了させる
    push.finish(() => {
      // 通知受信完了
    }, () => {
      // 通知受信終了処理に失敗
    }, data.additionalData.notId);  // カスタムデータで適当に ID を渡しておく
  });
  
  // エラー発生時に呼ばれるコールバック関数を定義しておく
  push.on('error', (error) => {
    console.log(error);
  });
});

ネイティブの場合、フォアグラウンドで動作中でも通知バーを表示するには userNotificationCenter() を自前で実装することで実現できるのだが、phonegap-plugin-push プラグインにはこの実装がないため、フォアグラウンド時に通知バーが表示されない。そのため、フォアグラウンド時に通知を受け取ったことをユーザに知らせるには、alert() などで通知しないといけない。フォアグラウンド通知の UI は cordova-plugin-dialogs か cordova-banner-notification あたりで実装するのがオススメ。

アプリが終了していたりバックグラウンドにある状態でプッシュ通知を受け取った時、通知バナーをタップせずにホーム画面のアプリアイコンからアプリを開くと、push.on('notification') のコールバック関数が実行されなかった。ユーザが通知を受け取った時に、必ずしも通知バナーからアプリを起動するとは限らないので、push.on('notification') の処理関数以外に、resume イベントなどでサーバに新着情報を問い合わせに行くようにしておくと良いだろう。

さて、基本的なプッシュ通知の受信はこれで準備完了。証明書用意の章の「手順 3.」で登録した UDID の端末を USB 接続し、以下のようにアプリをビルドしてデバイストークンを取得しておく。

  1. $ cordova build ios で Cordova ビルドを行う。
  2. ./platforms/ios/【アプリ名】.xcodeworkspace を開き、XCode ビルドを行い実機にアプリをインストールする。
  3. アプリ起動時に push.on('registration') が実行され、デバイストークンが取得される。上述のサンプルコードでは console.log() で出力しているので、デバイストークンの文字列を控えておく。

サーバサイドの証明書準備・Grocer によるサーバサイドの実装

アプリ側でリモートプッシュ通知の受信準備ができたら、次はプッシュ通知を送信するサーバ側を用意する。

今回は検証のため、Grocer という RubyGems を使い、簡単な Ruby コードでプッシュ通知を送信してみる。プッシュ送信するサーバには証明書ファイルを配置する必要があるため、証明書の準備から Grocer によるプッシュ通知送信のサンプル実装までを解説する。

  1. キーチェーンアクセスより、先程作成したプッシュ通知用の証明書「Apple Push Notification Service SSL (Sandbox)」.p12 形式で書き出す。
  2. キーチェーンアクセスの「分類」で「証明書」を選び、先程作成した証明書を探す。
  3. 当該「証明書」の配下に階層メニューで「秘密鍵」が格納されているはずなので、階層メニューを開いて「証明書」と「秘密鍵」の2項目を選択して右クリックし、「2項目を書き出す」を選択する (このように書き出さないと上手くいかない)。
  4. 「フォーマット」で「個人情報交換 (.p12)」を選択し、ローカルに保存する。
  5. 「書き出した項目を保護するために使用されるパスワード」は空欄で「OK」する。
  6. 以降の説明では cert.p12 というファイル名で保存したものとする。
  7. ターミナルにて、手順 1. で書き出した .p12 ファイルを .pem ファイルに変換する。
  8. ターミナルで、手順 1. で cert.p12 を保存したディレクトリに移動する。
  9. $ openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts -des3 というコマンドで PEM ファイルに変換する。
  10. コマンドを打つと、Enter Import Password と尋ねられるが、コレには未入力Enter する (手順 1. で書き出した時にパスワード設定した場合はココで入力することになるが、書き出し時にパスワードを設定すると正しく動作しないという情報があったため、手順 1. ではパスワードは未設定で行うこと)。
  11. 続いて Enter PEM Pass Phrase と尋ねられるので、任意のパスフレーズを入力する。このパスワードは後に送信側の設定で使用する。
  12. 以上を入力すると、コマンドの -out オプションで指定しているとおり cert.pem ファイルが生成される。
  13. $ sudo gem install grocer で、プッシュ送信用の RubyGems ライブラリ Grocer をインストールする。
  14. 任意のディレクトリにて、プッシュ送信を行う Ruby スクリプトファイルとして push.rb という名前のファイルを作成し、同ディレクトリに手順 2. で作成した cert.pem を格納する。
  15. push.rb を以下のようにコーディングする。
require 'grocer'

# 送信側の初期設定を行う
pusher = Grocer.pusher(
  certificate: "./cert.pem",                      # 手順 2. で作成した .pem ファイル
  passphrase:  "【手順 2. で設定したパスフレーズ】",
  gateway:     "gateway.sandbox.push.apple.com",  # Development の場合はこのアドレス
  port:        2195,                              # ポート (任意)
  retries:     3                                  # 送信失敗時のリトライ回数
)

# プッシュ通知の内容を設定する
notification = Grocer::Notification.new(
  # デバイストークンを指定する
  device_token: "【予め取得しておいた送信対象のデバイストークン】",
  # プッシュ通知のメッセージ
  alert: {
    title: "プッシュ通知タイトル",
    body: "プッシュ通知本文",
  },
  badge:  1,                   # 表示するバッジの数
  sound:  "default",           # 着信音
  expiry: Time.now + 60 * 60,  # 通知の有効期限
  # 任意のカスタムデータ
  custom: {
    "notId"   : "1"        # finish() 実行用の適当な ID を渡しておく
    "myParams": "example"  # その他通知に乗せて何かデータを送信したい場合はこのように渡せる (data.additionalData.myParams で取得可能)
  }
)

# プッシュ通知を送信する
pusher.push(notification)

以上で準備完了。

iOS 実機で Cordova アプリを用意しておき、端末から $ ruby push.rb で Ruby ファイルを実行してみよう。プッシュ通知が送信され、1・2秒で Cordova アプリ側で受信できるはずだ。当然ながらインターネット接続が可能な環境で行うこと。

オマケ 1 : 着信音の変更方法

ココまででリモートプッシュ通知が実装できたので十分ではあるが、せっかくなのでプラスアルファを。

クライアントアプリ側で予め音声ファイルを用意しておき、サーバ側でプッシュ送信時に sound プロパティを設定することで、着信音がカスタマイズできる。

  1. 任意の音声ファイルを AIFF 形式 (.aif) かCAFF 形式 (.caff) で用意する。
  2. 音声ファイルは30秒以内で、ファイルサイズが大きくなりすぎないようにすること。
  3. 自分は検証用に、「GarageBand」を使用して適当な音声ファイルを作成した。GarageBand の「共有」→「ディスクに書き出す」で AIFF (.aif) 形式で書き出したら、$ afconvert SRC-FILE.aif DEST-FILE.caf -d ima4 -f caff というコマンドで .caf 形式に変換して使用した。
  4. 本説明では mysound.caf というファイルを作成したものとする。
  5. XCode で .caff ファイルを追加する。
  6. .xcodeworkspace ファイルを開き、対象のプロジェクト → TARGETS の対象のプロジェクト → Build Phases タブ → Copy Bundle Resources → 「+」ボタンより、手順 1. で作成した .caff ファイルを追加する。
  7. 通常どおり XCode 上でアプリをビルドする。
  8. 送信側で、以下のように音声ファイル名を設定する (Grocer を使用する場合)。
notification = Grocer::Notification.new(
  device_token: "【デバイストークン】",
  alert:  "プッシュ通知 (本文のみ)",
  badge:  1,
  sound:  "mysound.caf",  # 音声ファイル名を指定する
  expiry: Time.now + 60 * 60,
  custom: {
    "notId": "1"
  }
)

これでプッシュ通知を送信すれば、任意の音声ファイルを再生させられる。当然ながら、端末がマナーモードの場合は音声は再生されない。

オマケ 2 : ActionButtons の使用方法

通知バーを開いたところに任意のボタンを最大3つまで配置でき、任意のコールバック関数を実行できる「ActionButtons」という機能があったので、これも実装してみた。

まず、クライアントアプリで以下のように ActionButtons を受け取るための各種定義を実装しておく。

// Action Buttons のコールバック関数 : グローバルに関数を定義しておく必要がある
window.actions = {
  // 「YES!」ボタン押下時のコールバック関数
  yes: function() {
    alert('YES!!');
  },
  // 「NO!」ボタン押下時のコールバック関数
  no: function() {
    alert('NO!!');
  },
  // 「MAYBE!」ボタン押下時のコールバック関数
  maybe: function() {
    alert('MAYBE!!');
  }
};

const push = window.PushNotification.init({
  ios: {
    alert: true,
    badge: true,
    sound: true,
    clearBadge: true,
    categories: {                   // Action Buttons 用の設定
      'mycategory': {               // 任意のカテゴリ名を定義しておく
        'yes': {                    // 'yes'・'no'・'maybe' の最大3種類のボタン情報が定義できる
          callback: 'actions.yes',  // コールバックで呼ぶ関数名を文字列で指定する
          title: 'YES!',            // ボタンのラベル
          foreground: true,         // 選択時にアプリをフォアグラウンドにするか
          destructive: false        // 選択肢のボタンを赤色にするか (何か破壊的な動作であることを示す)
        },
        'no': {
          callback: 'actions.no',
          title: 'NO!',
          foreground: true,
          destructive: true
        },
        'maybe': {
          callback: 'actions.maybe',
          title: 'MAYBE!',
          foreground: false,
          destructive: true
        }
      }
    }
  }
});

次に、サーバサイド (Grocer) でプッシュ送信する際に、実行する ActionButtons のカテゴリを指定する。

notification = Grocer::Notification.new(
  device_token: "【デバイストークン】",
  alert:    "プッシュ通知",
  badge:    1,
  sound:    "mysound.caf",
  expiry:   Time.now + 60 * 60,
  category: "mycategory",  # クライアントアプリで定義しておいたカテゴリ名を指定する
  custom: {
    "notId": "1"
  }
)

これでプッシュ通知を行い、受信した通知バーをスワイプすると、「YES!」「NO!」「MAYBE!」ボタンが表示される。

foreground プロパティの如何に関わらずコールバック関数は実行される。少なくとも console.log() は Safari インスペクタで確認できた。バックグラウンドの場合 alert() などネイティブの処理は無視される。

また、ColdStart で指定のコールバック関数を実行すると、push.on('notification') 内で data.additionalData.coldstart 判定して実行しようとした処理が呼ばれない様子。

phonegap-plugin-push プラグインのネイティブ実装を確認したが、callback プロパティに関数 (Function) を渡すことはできず、グローバルから特定の関数名を文字列で指定しないといけない。コレは少々使いづらいだろうか。

また、通知バーを単にタップしただけではどのボタンも選択したことにならないため、どのコールバック関数も実行されないパターンがある点も注意。通知に応じて確実に処理分岐を行いたいのであれば、通知を受信してからアプリ内で制御する方が確実。

その他

参考