(iOS)リモートプッシュ通知の実装方法
はじめに
本記事ではiOSアプリのプッシュ通知を実装します。記事を読み進めるための前提知識は不要、3分あれば通知を試すことができます。
なお、実装にあたりマネージドなモバイル通知サービス(Amazon SNSやFirebase Cloud Messaging)は利用しません。直接APNsにリクエストする方法を説明します。
「APNsって何だ?」と思われた方はご安心ください。すぐ後に説明します。
必要なもの
記事を読み進めるために、以下2点が必要になります。
- Apple Developerの登録
- 実機のiOSデバイス
本記事ではApple Developerの登録方法について説明しません。各自で調べてください。
2種類のプッシュ通知
実装へ進む前に用語の整理をしましょう。iOSのプッシュ通知は大まかに2種類あります。
- ローカルプッシュ: デバイス内部で送信される通知です。カレンダーアプリや時計アプリの通知が該当します。
- リモートプッシュ: デバイス外部から送信される通知です。インターネットを経由して任意のタイミングで通知できます。
本記事はリモートプッシュ通知の送信方法を説明します。
動作環境について
iOS 10.0以上であれば問題なく動作するはずです。古いデバイスが用意できなかったのですが、少なくともiOS 15.0からiOS 18.0の実機で動作することを確認済みです。
APNsとは?
Appleはリモートプッシュ通知を送信するためのWeb APIを提供しています。Apple Push Notification service (APNs)とよばれるサービスです。
Web APIですから、各プログラミング言語のHTTPライブラリを使用したり、curl
コマンドを利用したり、リクエストの方法は自由です。本記事ではcurl
コマンドを利用する方法で説明します。
なお、記事の最後にAPNsへリクエストする際の注意点をまとめます。
通知の流れ
デバイスに通知が配信されるまでの流れは以下のとおりです。
- アプリは通知の許可を尋ねます。
- ユーザーが許可すると、デバイスを識別するためのデバイストークンが得られます。
- デバイストークンを自社のサーバーに送信し、保管します。
- 自社のサーバーからAPNsにプッシュ通知の送信をリクエストします。このとき通知の対象者を指定するためにデバイストークンを使用します。
- ユーザーのデバイスにプッシュ通知が届きます。
本記事ではサーバーへ送信する代わりに、Xcodeのデバッグコンソールにデバイストークンを表示させます。その後、デバイストークンをコピーし、ターミナルからcurl
コマンドを使用してAPNsにリクエストします。
2種類の認証方法
第三者が勝手にプッシュ通知を送信できては困ります。そのためAPNsのリクエストには認証が要求されます。
認証には2種類の方法があり、JWT (JSON Web Token)を利用する方法と証明書を利用する方法が用意されています。特別な理由がない限り、新規にアプリを作成する場合はJWT方式を利用します。
プッシュ通知の利用料
プッシュ通知の送信に費用は発生しません。Apple Developerの年会費に含まれていると考えてください。
プッシュ通知に限った話題ではありませんが、Appleは暗黙に費用を請求しません。例えば地図関連のフレームワークMapKitは裏側でAppleの提供する地図サービスと通信していますが、追加の費用は発生しません。
実装手順
それではプッシュ通知を試してみましょう。以下の6ステップを順番に進めてください。
(ステップ1)Apple DeveloperサイトでJWT署名用の鍵を作成する
- Apple Developerのサイトを開きます。
- Certificates, IDs, & Profilesを開き、メニューの中からKeysを選びます。
- Addを押します。
- Key Nameにキーの名前を入力します。内容は適当で構いません。
- Key Usage Descriptionにキーの説明を入力します。内容は適当で構いません。
- Apple Push Notifications service (APNs)にチェックをつけます。
- Continueを押します。入力した内容に間違いがなければRegisterを押します。
- 画面が切り替わったらKey IDをメモします。Key IDは10桁の英数字です。
- Downloadを押します。
AuthKey_<Key ID>.p8
という名前のファイルがダウンロードされます。
※ダウンロード画面を閉じると.p8
ファイルの再取得はできません。間違えて画面を閉じてしまった場合は鍵の作成をやり直してください。
このステップは以上で完了です。
(ステップ2)アプリを新規作成する
- Xcodeを開きます。
- メニューからFile→New→Projectを選びます。
- iOSアプリを選択して、アプリを作成します。UIはSwiftUIを選んでください。
このステップは以上で完了です。
(ステップ3)アプリのCapabilitiesにPush Notificationsを追加する
- プロジェクト設定からSigning & Capabilitiesを選びます。
- Addボタンを押します。
- 検索フィールドに「push」と入力した後、Returnキーを押します。
- CapabilityにPush Notificationsが追加されたことを確認してください。
このステップは以上で完了です。
(ステップ4)アプリを実装する
エントリーポイントの.swift
ファイルを編集します。例えばアプリ名をSampleAppとして作成した場合、SampleApp.swift
を以下の内容で置き換えます。
import SwiftUI
import UIKit
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
self.registerPushNotifications()
return true
}
private func registerPushNotifications() {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if granted {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
} else {
print("Push notification authorization denied: \(error)")
}
}
}
func application(
_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("APNs Device Token: \(token)")
}
func application(
_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("Failed to register: \(error)")
}
}
@main
struct SampleApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
このステップは以上で完了です。
(ステップ5)デバイストークンをメモする
- Xcodeを開いた状態で
Command + R
キーを押して、アプリを実行します。 - 実機でアプリが起動したら、通知を許可します。
- Xcodeのデバッグコンソールを確認してください。「APNs Device Token: XXXXXXXX...」と表示されます。
- この64文字の英数字がデバイストークンです。メモしてください。
-
Command + Shift + K
キーを押してアプリを終了します。 - デバイスの電源ボタンを押して画面を消灯させておきます。
このステップは以上で完了です。
(ステップ6)プッシュ通知を送信する
以下のシェルスクリプトをtest.sh
など適当な名前で保存して、実行してください。空欄になっている変数はコメントに従って入力してください。
# ステップ1でメモしたKey IDを設定してください。
AUTH_KEY_ID=""
# ご自身のTeam ID(10桁の英数字)を指定してください。XcodeやApple Developerサイトで確認できます。
TEAM_ID=""
# ダウンロードした.p8ファイルのパスを指定してください。
TOKEN_KEY_FILE_NAME=""
# アプリのバンドルIDを指定してください。(例: com.example.SampleApp)
TOPIC=""
# ステップ5でメモしたデバイストークンを指定してください
DEVICE_TOKEN=""
# JWTを生成
JWT_ISSUE_TIME="$(date +%s)"
JWT_HEADER=$(printf '{"alg":"ES256","kid":"%s"}' "${AUTH_KEY_ID}" \
| openssl base64 -e -A \
| tr -- '+/' '-_' | tr -d =)
JWT_CLAIMS=$(printf '{"iss":"%s","iat":%d}' "${TEAM_ID}" "${JWT_ISSUE_TIME}" \
| openssl base64 -e -A \
| tr -- '+/' '-_' | tr -d =)
JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" \
| openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" \
| openssl base64 -e -A \
| tr -- '+/' '-_' | tr -d =)
AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}"
APNS_HOST_NAME="api.sandbox.push.apple.com"
# APNsにプッシュ通知の送信をリクエスト
curl -i \
--http2 \
--header "apns-topic: $TOPIC" \
--header "apns-push-type: alert" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data '{"aps":{"alert":"こんにちは!"}}' \
https://${APNS_HOST_NAME}/3/device/${DEVICE_TOKEN}
実行例
実行例を示します。200 OKステータスが返されたら成功です。
$ ./test.sh
HTTP/2 200
apns-id: 035F479F-9206-ACD2-6FB3-F874F6AA7691
apns-unique-id: 1f1fc16d-be8a-eb2c-754b-a69452943ef5
※成功した場合は空のレスポンスボディが返されます。
成功すると「こんにちは!」という通知が届くはずです。以上でプッシュ通知が送信できました!
実装の注意点
ここまでは手元で通知を試すだけでした。本番環境で動作するアプリを実装する際には追加の注意点があります。
なお、以下の注意点は最低限把握しておいた方が良い項目になります。本格的にプッシュ通知をアプリに組み込む場合は記事末尾の資料を参照してください。
デバイストークンは変化する可能性がある
デバイストークンは端末ごとに発行される識別子ですが、固定値ではありません。Apple Developerドキュメントから引用します。
Never cache device tokens in local storage. APNs issues a new token when the user restores a device from a backup, when the user installs your app on a new device, and when the user reinstalls the operating system. You get an up-to-date token each time you ask the system to provide the token.
(引用元)Registering your app with APNs - Apple Developer Documentation
APNsがサポートするのはHTTP/2のみ
APNsはHTTP/2のみサポートしています。記事の冒頭で各プログラミング言語のHTTPライブラリを自由に使えると説明しましたが、クライアントとしてHTTP/2をサポートしていない場合はリクエストが拒否されます。
Use HTTP/2 and TLS 1.2 or later to establish a connection between your provider server and APNs server to send API requests.
(引用元)Establishing a connection to Apple Push Notification service (APNs) - Apple Developer Documentation
HTTP/2コネクションの頻繁な接続と切断を避ける
Apple DeveloperドキュメントによるとHTTP/2コネクションの頻繁な接続と切断がある場合、DDoS攻撃と見なし、一時的にリクエストを拒否する可能性があると説明されています。
HTTP/2は1本のコネクションの中に複数のストリームを束ねるのが特徴です。従って、プッシュ通知をリクエストする都度コネクションの接続と切断を繰り返すのではなく、可能な限り同じコネクションを利用するべきです。
Check how often your provider server connects to APNs. If your provider server opens and closes its connection to APNs repeatedly, APNs may treat it as a denial-of-service attack and temporarily block your server from connecting.
(引用元)Troubleshooting push notifications - Apple Developer Documentation
JWTのリフレッシュ
本記事で説明したcurlコマンドでは、実行の都度opensslコマンドを用いてJWTを作成していました。これはあくまで手軽に試すための実装であり、JWTはリクエストの都度作成する必要はありません。
ただし、トークンの再利用には以下の制限があります。Apple Developerドキュメントから引用します。
For security, APNs requires you to refresh your token regularly. Refresh your token no more than once every 20 minutes and no less than once every 60 minutes. APNs rejects any request whose token contains a timestamp that’s more than one hour old. Similarly, APNs report an error if you use a new token more than once every 20 minutes on the same connection.
(引用元)Establishing a token-based connection to APNs - Apple Developer Documentation
APNsのスループットについて
記事を投稿した時点で、APNsのRate / Limitについて言及されているドキュメントは見つけられませんでした。APNsはBest effort serviceであるとの説明は見つかりますが、具体的なスループットを見積もるためのドキュメントは提供されていないようです。
実装の参考になる値として、HTTP/2のMax Concurrent Streamsがスループットを見積もる手がかりになるかもしれません。
(参考)Goのnet/httpクライアントで大量のリクエストを高速に行う方法 - Fenrir Engineers
おわりに
単純なプッシュ通知の送信を試すだけでしたら、お手軽に実現できます。ただし、実装の注意点で説明したように、本番環境の運用に耐えるような高頻度かつ大量の通知を送信するにはそれなりの考慮が必要になります。
自前の配信基盤を開発して運用するのは技術的におもしろいですが、アプリの差別化機能でなければマネージドな通知配信サービスを採用する方が無難かと思います。
Discussion