プッシュ通知 API リファレンス
APNs HTTP/2 トランスポート、UserNotifications フレームワーク、Live Activities やブロードキャストプッシュなどのプッシュ主導機能に関する包括的な API リファレンスです。
クイックリファレンス
// AppDelegate — minimal remote notification setup
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
sendTokenToServer(token)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Registration failed: \(error)")
}
// Show notifications when app is in foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
return [.banner, .sound, .badge]
}
// Handle notification tap / action response
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let userInfo = response.notification.request.content.userInfo
// Route to appropriate screen based on userInfo
}
}
APNs トランスポートリファレンス
エンドポイント
| 環境 | ホスト | ポート |
|---|
| 開発 | api.sandbox.push.apple.com | 443 または 2197 |
| 本番 | api.push.apple.com | 443 または 2197 |
リクエスト形式
POST /3/device/{device_token}
Host: api.push.apple.com
Authorization: bearer {jwt_token}
apns-topic: {bundle_id}
apns-push-type: alert
Content-Type: application/json
APNs ヘッダー
| ヘッダー | 必須 | 値 | 備考 |
|---|
| apns-push-type | はい | alert, background, liveactivity, voip, complication, fileprovider, mdm, location | ペイロードの内容と一致する必要があります |
| apns-topic | はい | Bundle ID(または .push-type.liveactivity サフィックス) | トークン認証で必須です |
| apns-priority | いいえ | 10(即時)、5(電力消費を抑制)、1(低) | デフォルト:alert は 10、background は 5 |
| apns-expiration | いいえ | UNIX タイムスタンプまたは 0 | 0 = 1 回配信し、保存しません |
| apns-collapse-id | いいえ | 64 バイト以下の文字列 | デバイス上の一致する通知を置き換えます |
| apns-id | いいえ | UUID(小文字) | トラッキング用に APNs から返されます |
| authorization | トークン認証 | bearer {JWT} | 証明書認証では不要です |
| apns-unique-id | レスポンスのみ | UUID | Push Notifications Console の配信ログと併せて使用します |
レスポンスコード
| ステータス | 意味 | 一般的な原因 |
|---|
| 200 | 成功 | |
| 400 | 不正なリクエスト | JSON の形式不正、必須ヘッダーの欠落 |
| 403 | アクセス禁止 | JWT の期限切れ、チーム/キーの誤り、トピックの不一致 |
| 404 | 見つかりません | デバイストークンのパスが無効 |
| 405 | メソッドが許可されていません | POST を使用していない |
| 410 | 登録解除済み | デバイストークンが無効になっています(アプリがアンインストールされています) |
| 413 | ペイロードが大きすぎます | 4KB を超過(VoIP は 5KB) |
| 429 | リクエストが多すぎます | APNs によるレート制限 |
| 500 | 内部サーバーエラー | APNs の問題。再試行してください |
| 503 | サービスを利用できません | APNs の過負荷。バックオフを適用して再試行してください |
JWT 認証リファレンス
JWT ヘッダー
{ "alg": "ES256", "kid": "{10-char Key ID}" }
JWT クレーム
{ "iss": "{10-char Team ID}", "iat": {unix_timestamp} }
ルール
| ルール | 詳細 |
|---|
| アルゴリズム | ES256(P-256 曲線) |
| 署名キー | APNs 認証キー(開発者ポータルから取得する .p8) |
| トークンの有効期間 | 最大 1 時間(それを超えると 403 ExpiredProviderToken) |
| 更新間隔 | 20~60 分 |
| スコープ | 1 つのキーでチーム内のすべてのアプリと両方の環境に対応します |
Authorization ヘッダー形式
authorization: bearer eyAia2lkIjog...
ペイロードリファレンス
aps 辞書のキー
| キー | 型 | 用途 | 対応バージョン |
|---|
| alert | Dict/String | アラートの内容 | iOS 10 |
| badge | Number | アプリアイコンのバッジ(0 で削除) | iOS 10 |
| sound | String/Dict | 音声の再生 | iOS 10 |
| thread-id | String | 通知のグループ化 | iOS 10 |
| category | String | アクション可能な通知の種類 | iOS 10 |
| content-available | Number (1) | サイレントなバックグラウンドプッシュ | iOS 10 |
| mutable-content | Number (1) | サービス拡張機能を起動 | iOS 10 |
| target-content-id | String | ウィンドウ/コンテンツ識別子 | iOS 13 |
| interruption-level | String | passive/active/time-sensitive/critical | iOS 15 |
| relevance-score | Number 0-1 | 通知の要約での並べ替え | iOS 15 |
| filter-criteria | String | 集中モードフィルターとの照合 | iOS 15 |
| stale-date | Number | UNIX タイムスタンプ(Live Activity) | iOS 16.1 |
| content-state | Dict | Live Activity のコンテンツ更新 | iOS 16.1 |
| timestamp | Number | UNIX タイムスタンプ(Live Activity) | iOS 16.1 |
| event | String | start/update/end(Live Activity) | iOS 16.1 |
| dismissal-date | Number | UNIX タイムスタンプ(Live Activity) | iOS 16.1 |
| attributes-type | String | Live Activity の構造体名 | iOS 17 |
| attributes | Dict | Live Activity の初期化データ | iOS 17 |
アラート辞書のキー
| キー | 型 | 用途 |
|---|
| title | String | 短いタイトル |
| subtitle | String | 補足の説明 |
| body | String | メッセージ全文 |
| launch-image | String | 起動画面のファイル名 |
| title-loc-key | String | タイトルのローカライズキー |
| title-loc-args | [String] | タイトルの書式引数 |
| subtitle-loc-key | String | サブタイトルのローカライズキー |
| subtitle-loc-args | [String] | サブタイトルの書式引数 |
| loc-key | String | 本文のローカライズキー |
| loc-args | [String] | 本文の書式引数 |
サウンド辞書(クリティカルアラート)
{ "critical": 1, "name": "alarm.aiff", "volume": 0.8 }
割り込みレベルの値
| 値 | 動作 | 必要条件 |
|---|
| passive | 音も画面の点灯もありません。通知の要約のみです。 | なし |
| active | デフォルトです。サウンドとバナーが表示されます。 | なし |
| time-sensitive | 配信予定を中断します。バナーが表示され続けます。 | Time Sensitive 機能 |
| critical | おやすみモードと着信/通知音スイッチを無視します。 | Apple の承認とエンタイトルメント |
ペイロードの例
基本的なアラート
{
"aps": {
"alert": {
"title": "New Message",
"subtitle": "From Alice",
"body": "Hey, are you free for lunch?"
},
"badge": 3,
"sound": "default"
}
}
loc-key/loc-args によるローカライズ
{
"aps": {
"alert": {
"title-loc-key": "MESSAGE_TITLE",
"title-loc-args": ["Alice"],
"loc-key": "MESSAGE_BODY",
"loc-args": ["Alice", "lunch"]
},
"sound": "default"
}
}
サイレントなバックグラウンドプッシュ
{
"aps": {
"content-available": 1
},
"custom-key": "sync-update"
}
リッチ通知(サービスエクステンション)
{
"aps": {
"alert": {
"title": "Photo shared",
"body": "Alice shared a photo with you"
},
"mutable-content": 1,
"sound": "default"
},
"image-url": "https://example.com/photo.jpg"
}
クリティカルアラート
{
"aps": {
"alert": {
"title": "Server Down",
"body": "Production database is unreachable"
},
"sound": { "critical": 1, "name": "default", "volume": 1.0 },
"interruption-level": "critical"
}
}
カテゴリ付きの Time-Sensitive 通知
{
"aps": {
"alert": {
"title": "Package Delivered",
"body": "Your order has been delivered to the front door"
},
"interruption-level": "time-sensitive",
"category": "DELIVERY",
"sound": "default"
},
"order-id": "12345"
}
UNUserNotificationCenter API リファレンス
主なメソッド
| メソッド | 用途 |
|---|
| requestAuthorization(options:) | 許可をリクエストする |
| notificationSettings() | 現在の状態を確認する |
| add(_:) | 通知リクエストをスケジュールする |
| getPendingNotificationRequests() | スケジュール済みの通知を一覧表示する |
| removePendingNotificationRequests(withIdentifiers:) | スケジュール済みの通知をキャンセルする |
| getDeliveredNotifications() | 通知センター内の通知を一覧表示する |
| removeDeliveredNotifications(withIdentifiers:) | 通知センターから削除する |
| setNotificationCategories(_:) | アクション可能な種類を登録する |
| setBadgeCount(_:) | バッジを更新する(iOS 16 以降) |
| supportsContentExtensions | コンテンツエクステンションのサポートを確認する |
UNAuthorizationOptions
| オプション | 用途 |
|---|
| .alert | アラートを表示する |
| .badge | バッジ数を更新する |
| .sound | サウンドを再生する |
| .carPlay | CarPlay に表示する |
| .criticalAlert | クリティカルアラート(エンタイトルメントが必要) |
| .provisional | プロンプトを表示せずに試験的に配信する |
| .providesAppNotificationSettings | 設定画面に「アプリで設定」ボタンを表示する |
| .announcement | Siri による読み上げ(iOS 15 以降は非推奨) |
UNAuthorizationStatus
| 値 | 意味 |
|---|
| .notDetermined | まだプロンプトが表示されていません |
| .denied | ユーザーが拒否したか、設定で無効にしました |
| .authorized | ユーザーが明示的に許可しました |
| .provisional | 試験的な配信が許可されています |
| .ephemeral | App Clip の一時的な許可です |
許可をリクエストする
let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
if granted {
await MainActor.run {
UIApplication.shared.registerForRemoteNotifications()
}
}
設定を確認する
let settings = await center.notificationSettings()
switch settings.authorizationStatus {
case .authorized: break
case .denied:
// Direct user to Settings
case .provisional:
// Upgrade to full authorization
case .notDetermined:
// Request authorization
case .ephemeral:
// App Clip — temporary
@unknown default: break
}
デリゲートメソッド
// Foreground presentation — called when notification arrives while app is active
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async
-> UNNotificationPresentationOptions {
return [.banner, .sound, .badge]
}
// Action response — called when user taps notification or action button
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let actionIdentifier = response.actionIdentifier
let userInfo = response.notification.request.content.userInfo
switch actionIdentifier {
case UNNotificationDefaultActionIdentifier:
// User tapped notification body
break
case UNNotificationDismissActionIdentifier:
// User dismissed (requires .customDismissAction on category)
break
default:
// Custom action
break
}
}
// Settings — called when user taps "Configure in App" from notification settings
func userNotificationCenter(_ center: UNUserNotificationCenter,
openSettingsFor notification: UNNotification?) {
// Navigate to in-app notification settings
}
UNNotificationCategory と UNNotificationAction の API
カテゴリの登録
let likeAction = UNNotificationAction(
identifier: "LIKE",
title: "Like",
options: []
)
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY",
title: "Reply",
options: [],
textInputButtonTitle: "Send",
textInputPlaceholder: "Type a message..."
)
let deleteAction = UNNotificationAction(
identifier: "DELETE",
title: "Delete",
options: [.destructive, .authenticationRequired]
)
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE",
actions: [likeAction, replyAction, deleteAction],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: "New message",
categorySummaryFormat: "%u more messages",
options: [.customDismissAction]
)
UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
アクションのオプション
| オプション | 効果 |
|---|
| .authenticationRequired | デバイスのロック解除が必要です |
| .destructive | 赤い文字で表示されます |
| .foreground | アプリをフォアグラウンドで起動します |
カテゴリのオプション
| オプション | 効果 |
|---|
| .customDismissAction | 通知を閉じたときにデリゲートを呼び出します |
| .allowInCarPlay | CarPlay にアクションを表示します |
| .hiddenPreviewsShowTitle | プレビューが非表示の場合にタイトルを表示します |
| .hiddenPreviewsShowSubtitle | プレビューが非表示の場合にサブタイトルを表示します |
| .allowAnnouncement | Siri による読み上げを許可します(iOS 15 以降は非推奨) |
UNNotificationActionIcon(iOS 15 以降)
let icon = UNNotificationActionIcon(systemImageName: "hand.thumbsup")
let action = UNNotificationAction(
identifier: "LIKE",
title: "Like",
options: [],
icon: icon
)
UNNotificationServiceExtension API
通知を表示する前に、その内容を変更します。別のエクステンションプロセスで実行されます。
ライフサイクル
| メソッド | 時間 | 用途 |
|---|
| didReceive(_:withContentHandler:) | 約30秒 | 通知の内容を変更する |
| serviceExtensionTimeWillExpire() | 期限に呼び出されます | 可能な範囲で最善の内容を直ちに配信する |
実装
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
guard let content = bestAttemptContent,
let imageURLString = content.userInfo["image-url"] as? String,
let imageURL = URL(string: imageURLString) else {
contentHandler(request.content)
return
}
// Download and attach image
let task = URLSession.shared.downloadTask(with: imageURL) { url, _, error in
defer { contentHandler(content) }
guard let url = url, error == nil else { return }
let attachment = try? UNNotificationAttachment(
identifier: "image",
url: url,
options: [UNNotificationAttachmentOptionsTypeHintKey: "public.jpeg"]
)
if let attachment = attachment {
content.attachments = [attachment]
}
}
task.resume()
}
override func serviceExtensionTimeWillExpire() {
if let content = bestAttemptContent {
contentHandler?(content)
}
}
}
サポートされている添付ファイルの種類
| 種類 | 拡張子 | 最大サイズ |
|---|
| 画像 | .jpg, .gif, .png | 10 MB |
| オーディオ | .aif, .wav, .mp3 | 5 MB |
| ビデオ | .mp4, .mpeg | 50 MB |
ペイロードの要件
サービス拡張機能を起動するには、通知ペイロードの aps 辞書に "mutable-content": 1 を含める必要があります。
ローカル通知 API
トリガーの種類
| トリガー | 用途 | 繰り返し |
|---|
| UNTimeIntervalNotificationTrigger | N 秒後 | 可(≥60s) |
| UNCalendarNotificationTrigger | 特定の日時 | 可 |
| UNLocationNotificationTrigger | 領域への進入/退出 | 可 |
時間間隔トリガー
let content = UNMutableNotificationContent()
content.title = "Reminder"
content.body = "Time to take a break"
content.sound = .default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 300, repeats: false)
let request = UNNotificationRequest(
identifier: "break-reminder",
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
カレンダートリガー
var dateComponents = DateComponents()
dateComponents.hour = 9
dateComponents.minute = 0
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
let request = UNNotificationRequest(
identifier: "daily-9am",
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
位置情報トリガー
import CoreLocation
let center = CLLocationCoordinate2D(latitude: 37.3349, longitude: -122.0090)
let region = CLCircularRegion(center: center, radius: 100, identifier: "apple-park")
region.notifyOnEntry = true
region.notifyOnExit = false
let trigger = UNLocationNotificationTrigger(region: region, repeats: false)
let request = UNNotificationRequest(
identifier: "arrived-at-office",
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
制限事項
| 制限事項 | 詳細 |
|---|
| 最小繰り返し間隔 | UNTimeIntervalNotificationTrigger の場合は 60 秒 |
| 位置情報の認証 | 位置情報トリガーには「使用中のみ」または「常に」の認証が必要です |
| サービス拡張機能なし | ローカル通知では UNNotificationServiceExtension は起動しません |
| バックグラウンドでの起動なし | ローカル通知ではバックグラウンド処理に content-available を使用できません |
| アプリ拡張機能 | アプリ拡張機能からローカル通知をスケジュールすることはできません(アプリグループとメインアプリを使用してください) |
| 保留数の上限 | アプリごとに保留中の通知リクエストは 64 件までです |
Live Activity のプッシュヘッダー
必須ヘッダー
| ヘッダー | 値 |
|---|
| apns-push-type | liveactivity |
| apns-topic | {bundleID}.push-type.liveactivity |
| apns-priority | 5(通常)または 10(時間制限あり) |
イベントの種類
| イベント | 目的 | 必須フィールド |
|---|
| start | Live Activity をリモートで開始します | attributes-type、attributes、content-state、timestamp |
| update | コンテンツを更新します | content-state、timestamp |
| end | Live Activity を終了します | timestamp(content-state は任意) |
更新ペイロード
{
"aps": {
"timestamp": 1709913600,
"event": "update",
"content-state": {
"homeScore": 2,
"awayScore": 1,
"inning": "Top 7"
}
}
}
開始ペイロード(プッシュ開始トークン)
{
"aps": {
"timestamp": 1709913600,
"event": "start",
"content-state": {
"homeScore": 0,
"awayScore": 0,
"inning": "Top 1"
},
"attributes-type": "GameAttributes",
"attributes": {
"homeTeam": "Giants",
"awayTeam": "Dodgers"
},
"alert": {
"title": "Game Starting",
"body": "Giants vs Dodgers is about to begin"
}
}
}
開始ペイロード(チャンネルベース)
{
"aps": {
"timestamp": 1709913600,
"event": "start",
"content-state": {
"homeScore": 0,
"awayScore": 0,
"inning": "Top 1"
},
"attributes-type": "GameAttributes",
"attributes": {
"homeTeam": "Giants",
"awayTeam": "Dodgers"
}
}
}
終了ペイロード
{
"aps": {
"timestamp": 1709913600,
"event": "end",
"dismissal-date": 1709917200,
"content-state": {
"homeScore": 5,
"awayScore": 3,
"inning": "Final"
}
}
}
プッシュ開始トークン
// Observe push-to-start tokens (iOS 17.2+)
for await token in Activity<GameAttributes>.pushToStartTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
sendPushToStartTokenToServer(tokenString)
}
アクティビティプッシュトークン
// Observe activity-specific push tokens
for await tokenData in activity.pushTokenUpdates {
let token = tokenData.map { String(format: "%02x", $0) }.joined()
sendActivityTokenToServer(token, activityId: activity.id)
}
コンテンツ状態のエンコード規則:システムは常にデフォルトの JSONDecoder を使用します。ActivityAttributes.ContentState でカスタムのエンコード戦略を使用しないでください。
ブロードキャストプッシュ API(iOS 18 以降)
個々のデバイストークンを追跡せずに Live Activities を複数のユーザーにプッシュするサーバー機能です。
エンドポイント
POST /4/broadcasts/apps/{TOPIC}
ヘッダー
| ヘッダー | 値 |
|---|
| apns-push-type | liveactivity |
| apns-channel-id | {channelID} |
| authorization | bearer {JWT} |
チャンネル経由でのサブスクライブ
try Activity.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil),
pushType: .channel(channelId)
)
チャンネルのストレージポリシー
| ポリシー | 動作 | 予算 |
|---|
| ストレージなし | 接続中のデバイスにのみ配信します | 多い |
| 最新メッセージ | オフラインのデバイス向けに最新のメッセージを保存します | 少ない |
コマンドラインでのテスト
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}"
アラートプッシュの送信
curl -v \
--header "apns-topic: $TOPIC" \
--header "apns-push-type: alert" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data '{"aps":{"alert":"test"}}' \
--http2 https://${APNS_HOST_NAME}/3/device/${DEVICE_TOKEN}
Live Activity プッシュの送信
curl \
--header "apns-topic: com.example.app.push-type.liveactivity" \
--header "apns-push-type: liveactivity" \
--header "apns-priority: 10" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data '{
"aps": {
"timestamp": '$(date +%s)',
"event": "update",
"content-state": { "score": "2-1" }
}
}' \
--http2 https://api.sandbox.push.apple.com/3/device/$ACTIVITY_PUSH_TOKEN
シミュレーターへのプッシュ
xcrun simctl push booted com.example.app payload.json
シミュレーターのペイロードファイル
{
"Simulator Target Bundle": "com.example.app",
"aps": {
"alert": { "title": "Test", "body": "Hello" },
"sound": "default"
}
}
リソース
WWDC:2021-10091、2023-10025、2023-10185、2024-10069
ドキュメント:/usernotifications、/usernotifications/sending-notification-requests-to-apns、/usernotifications/generating-a-remote-notification、/activitykit
スキル:axiom-push-notifications、axiom-push-notifications-diag、axiom-extensions-widgets