ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
プッシュ通知によるライブアクティビティの更新
Apple Push Notificationサービス(APNs)を通じてコンテンツをプッシュする際に、リモートでアプリのライブアクティビティを更新する方法を紹介します。最初のライブアクティビティのためのプッシュをローカルで設定する方法を紹介し、実装をすばやく反復する方法について解説するとともに、プッシュの優先順位を決定し、アラートアップデートを設定するためのベストプラクティスや、関連性スコアと古くなった日付を使用してライブアクティビティをさらに改善する方法について解説します。 このセッションを最大限に活用するためには、ActivityKitとライブアクティビティに精通していることが望ましいです。ライブアクティビティの概要については「ActivityKiについてt」をご確認ください。
関連する章
- 0:00 - Intro
- 2:10 - Preparations
- 5:58 - First push update
- 11:04 - Priority and alerts
- 15:40 - Enhancements
- 17:27 - Wrap-up
リソース
- ActivityKit
- Establishing a token-based connection to APNs
- Human Interface Guidelines: Live Activities
- Sending notification requests to APNs
- Sending push notifications using command-line tools
- Starting and updating Live Activities with ActivityKit push notifications
関連ビデオ
WWDC24
WWDC23
-
ダウンロード
♪ ♪
Jeff: Live Activities Teamの エンジニアのJeffです プッシュ通知によるライブアクティビティの 更新について共有できることを 嬉しく思います ライブアクティビティは進行中の アクティビティに関する情報を一目で 確認できるように表示する 素晴らしい方法です ActivityKitはアプリがライブアクティビティを 開始 更新 終了できるようにします そしてWidgetKitとSwiftUIを利用することで ユーザに情報を表示する UIを構築することができます
これらの技術をもっと知りたい場合は こちらをご確認ください Canによる「ActivityKitについて」 というセッションです このセッションで Emoji Rangersに新しい ライブアクティビティを追加して ヒーローの 冒険のステータスを表示できます でも主人公に仲間がいれば もっと楽しくなると思います そこで複数のユーザがヒーローと チームを組み一緒に冒険をすることができる 新機能を追加したいと思います 最高の体験を提供するために ライブアクティビティを更新し チーム内の全ヒーローのイベントが 表示されるように更新します
そのためにデバイス上でアドベンチャーの 追跡をするのではなく サーバーを導入します サーバーによってライブアクティビティを 最新の状態に保てるようにします 計算はサーバー上で行われるので アプリはライブアクティビティを 更新するためにフォアグラウンドの ランタイムを必要としません これにより ユーザの バッテリー寿命に与える 影響が少なくなります ActivityKitのプッシュ通知でライブ アクティビティを更新するのは この機能を実行する 素晴らしい方法になる見込みです このセッションではまずプッシュアップデートで ライブアクティビティを 更新できるようにするために 必要な準備について説明します 次に最初のプッシュアップデートを コンピューターから送信します そして 更新時のプライオリティと ユーザへのアラート方法の違いについて 掘り下げます 最後にプッシュアップデートを さらにレベルアップさせるために 追加できる機能強化について説明します さっそく準備に取りかかりましょう プッシュアップデートでライブ アクティビティの更新を始める前に アプリとサーバーがAppleプッシュ通知 サービスとどのようにやりとりするかを 理解しておくと便利です すべてはアプリから始まります 新しいアクティビティが開始されると ActivityKitは Appleプッシュ通知サービス(APNs)から プッシュトークンを取得します このプッシュトークンはリクエストした ライブアクティビティごとに固有です そのためアプリがプッシュアップデートの 送信を開始する前に それをサーバーに送信する 必要があります ライブアクティビティを更新する 必要があるときは 常にサーバーはAPNsにトークンを使って プッシュリクエストを送信します 最後にAPNsはペイロードをデバイスに送信し Widget Extentionを 起動してUIをレンダリングします
この新機能をサポートするために APNsは新しい "liveactivity" プッシュタイプを 導入しました このプッシュタイプは APNsにトークンベースで 接続しているサーバーでのみ 利用できます プッシュリクエストの送信について詳しくは 「APNsへの通知リクエストの送信」 ドキュメントを参照してください トークンベースの接続に関しては 「APNsへのトークンベース接続の確立」 を参照してください 次のステップはライブアクティビティが プッシュアップデートを受信するように アプリを修正することです Xcodeでアプリのターゲットに移動し "Signing & Capabilities" タブで プッシュ通知機能を追加します これによりActivityKitがアプリに代わって プッシュトークンをリクエスト 可能になります これからコードに入りたいと思います ライブアクティビティをリクエストする Emoji Rangersのコードの 一部分です アクティビティリクエストメソッドに アドベンチャーの属性とコンテンツの 初期状態を与えます プッシュの受信をサポートする メソッドにpushTypeパラメータを追加し その値を "token" に設定します これによりActivityKitはライブ アクティビティの作成時に プッシュトークンを要求できます アクティビティが作成されたら アプリはプッシュトークンを サーバーに送信する必要があります アクティビティタイプには プッシュトークンに 同期的にアクセスできる pushTokenプロパティがあります ただしアクティビティ作成直後は アクセスしないでください 得られる値はほとんどの場合ゼロです これは非同期処理だからです またアクティビティのライフタイム中に システムがプッシュトークンを 更新することも可能です そのためアプリはそれに応じて 対応する必要があります プッシュトークンを 適切に処理する方法は まず非同期タスクを作成し アクティビティのpushToken更新の 非同期配列から 値を観察する for-awaitループを開始します forループ内のコードは ライブアクティビティに 新しいプッシュトークンがあるたびに 実行されます ここで非同期のforループを使うことが 重要なのは 最初のプッシュトークンだけでなく それ以降のプッシュトークンの更新も 処理できるようにするためです トークンを受け取ったら それを16進文字列に変換し デバッグコンソールに記録します これは次のテストに役立つはずです そして最後にアプリに必要な 他のデータと一緒に プッシュトークンを サーバーに送信します プッシュトークンは アクティビティごとに異なり ユーザが開始する ライブアクティビティごとに 追跡することが重要です またシステムが既存のアクティビティに 新しいプッシュトークンを要求すると アプリはフォアグラウンドで実行されます 新しいプッシュトークンをサーバーに 正しく送信されるように 古いプッシュトークンを 無効にすることが重要です 準備が完了し 最初のプッシュ アップデートを送信する時が来ました プッシュアップデートを送信するにはAPNに HTTPリクエストを送信する必要があります リクエストはAPNsヘッダと APNsペイロードの2つの部分で構成されます 通常のHTTPヘッダに加え 3つのヘッダを 提供することが必要です 1つ目はapns-push-typeで 値は"liveactivity"です 次はapns-topicで アプリのバンドルIDに ".push-type.liveactivity"が続きます 3つ目はapns-priorityで 5か10の値を指定します 5はこのプッシュリクエストの優先度が 低いことを示し 10は優先度が高いことを示します ライブアクティビティが即座に 更新されるため テスト中は高い優先順位を使います 最初のAPNsペイロードは 3つのフィールドで構成されています 1つ目は "timestamp" で 1970年からの経過秒です システムはタイムスタンプを使用して 常に最新のコンテンツ状態を レンダリングするようにしています 2つ目は "event"です ライブアクティビティで実行したい アクションです その値は"update"か"end"のどちらかです この最初のAPNsリクエストでは "update" に設定すべきでしょう 3番目のフィールドは "content-state" です これはアクティビティの コンテンツステートタイプに デコードできるJSONオブジェクトです コンテンツの状態を確実に取得するために アプリ内からFoundationの JSONEncoder タイプを使用できます ここではライブアクティビティの Content Stateインスタンスを作成し そしてJSONEncoderを インスタンス化します 最後にコンテンツステートを JSONデータにエンコードし その文字列表現をコンソールに記録します キャメルケースのキーを持つこの JSON出力は私が予想していた通りです コンテンツステートのJSONは常に デフォルトのデコードストラテジーを持つ JSONDecoderを使用してデコードされます そのためコンテンツステートを エンコードする際には カスタムエンコーディングストラテジーを 設定しないことです 設定してあるとJSONは不一致となり システムはライブアクティビティの 更新に失敗してしまいます プッシュリクエストの 内容がわかったところで 次はプッシュリクエストの送信テストです 開発中に素早く繰り返せることに 大賛成です ですのでサーバーを変更することなく ライブアクティビティのプッシュ通知を テストするのが良いでしょう 私の端末から直接APNsに プッシュリクエストを 送ることで実現します これを実行するために コマンドラインを設定します 「コマンドラインツールによる プッシュ通知の送信」をご確認ください 「トークンを使ったプッシュ通知の送信」の セクションの指示に従っていることを 確認してください 認証トークン変数をプリントすれば すべてが正しく設定されていることを すぐに確認できます 次に必要な情報はプッシュトークンです 前のセクションではプッシュトークンを コンソールに記録するコード を追加しました ですのでそこから取ってこようと思ってます 同じアプローチを取った場合 アプリをデバイスにデプロイし ライブアクティビティを開始します アプリは アクティビティが開始された直後に プッシュトークンを記録します プッシュトークンをコピーしターミナルで アクティビティプッシュトークン変数として 設定します APNsリクエストを送信するには curlコマンドを実行します これは私がアドベンチャー ライブアクティビティ用に 作ったものです 「apns-topic」ヘッダにアプリの バンドルIDの後にプッシュタイプの サフィックスが設定されます そして「apns-push-type 」ヘッダは "liveactivity" に設定されます 第三に「apns-priority 」が10に 設定されているので このリクエストはすぐに配信されます 最後のHTTPヘッダ "authorization" には "bearer" が設定され その後に認証トークン変数が続きます データに関してはAPNsの ペイロード全体が含まれています 秒単位まで正確な数字を保証するために dateコマンドを使って タイムスタンプを 自動的に作成しています 最後にURLについてHTTP2を 使用していることを確認してください。 そしてURLの最後に前のステップで設定した アクティビティプッシュトークン変数を 参照します これで終わりです このcurlコマンドを実行すると ライブアクティビティは ペイロードで提供された 新しいコンテンツステートで更新されます ライブアクティビティが更新されると 思っていたのに 更新されないことがあります curlコマンドの実行時に エラーレスポンスが ないことを確認すべきです エラーはリクエストの フィールドが正しくないか 環境設定時に問題があったのか 環境設定時に問題があった 可能性があります APNsが正常な応答を 返したにもかかわらず ライブアクティビティが 更新されない場合は Consoleアプリを使用して デバイスのログを表示し 問題の特定を試みることができます 関連するログを持つ可能性のある プロセスはliveactivitiesd apsd chronodです ライブアクティビティがプッシュ通知で どのように更新されるかに満足したら 本物のプッシュアップデートを 送信し始めるために サーバーを変更する時です そしてユーザ体験をデザインする上で 重要な部分である プライオリティとアラートに行き着きます 最高のユーザ体験を保証するためには 各アップデートのプッシュプライオリティを 正しく選択することが重要です 常に優先して使用することを考慮すべき なのはロープライオリティです プライオリティの低いアップデートは 臨機応変に配信されるため ユーザのバッテリー寿命への 影響を低減します しかしこれはプッシュ要求が 送信されたときに ライブアクティビティが すぐに更新されない可能性があることを 意味します ですから時間的な制約の少ない更新には ロープライオリティを使うべきです 私のアドベンチャー ライブアクティビティでは 共通の戦利品を見つけたり ヒーローが多少のヘルスポイントを 回復するようなアップデートは ユーザの即時の注意を必要としません だからこそロープライオリティの アップデートを使うのに最適なのです ロープライオリティを使うもう一つの利点は 送信できる更新回数に 制限がないことです これを利用するためには ライブアクティビティの更新の大半に ロープライオリティを使用する必要があります 一方でヒーローが倒されたときや 大きなボスを倒したときなど ユーザの即時の注意を必要とする 更新もあります このような場合はハイプラオリティの アップデートを選びます ハイプライオリティのアップデートは 即座に配信されます だからこそ 一刻を争うアップデートに 最適なのです しかしユーザのバッテリー寿命に 影響を与えるためシステムは デバイスの状態に応じて予算を課します アプリが予算を超過した場合 プッシュアップデートが制限され ユーザ体験に多大な 影響を与えます ご自身のアプリのことはご自身が一番よく 知っていらっしゃるはずですので どのプライオリティをどのアップデートに 使うべきかを慎重に 検討することが重要です 「Emoji Rangers」では パーティが次々と大ボスと戦う 特殊な冒険を紹介します この集中的な ライブアクティビティで 最高のユーザ体験を提供するためには サーバーがハイプライオリティのプッシュを 頻繁に送信して最新状態を 維持する必要があります このためにライブアクティビティの 頻繁な更新機能を有効にします この機能を有効にすると アプリの更新予算が高くなり ライブアクティビティの更新が スロットルされにくくなります この機能を採用するには info.plistに NSSupportsLiveActivitiesFrequentUpdates という新しいキーを追加し その値をYESに設定します ユーザは設定アプリで ライブアクティビティとは別に この頻繁なアップデート機能を 無効にすることができます そこでActivityAuthorizationInfo frequentPushesEnabledプロパティに アクセスすることで この機能が有効かどうかを検出できます サーバーはこの値に応じて更新頻度を 調整すべきですので プッシュ更新の送信を開始する前に サーバーにこの値を送信していることを 確認してください この値をチェックするのは アクティビティ開始後 一度だけで大丈夫です この値が変更されるとシステムは 進行中のすべてのアクティビティを 終了するので サーバーが頻繁に更新が切り替わることを 心配する必要はないです アドベンチャーライブアクティビティでは ヒーローが倒されたとき 即座に更新するだけでなく ユーザの注意を引きすぐにアプリに入って 回復薬を使えるようにしたいです そのために3つのフィールドを持つ "alert" オブジェクトを ペイロードに追加します "title" は通知のタイトル になります "body" はアップデートについての 短いメッセージ になります "sound" はアラートが起動されたときに 再生されるサウンドを示します Emoji Rangersは 複数の言語に対応していますので 英語でのアラート送信だけは 理想的ではありません サーバーでは非常に扱いにくいです 幸いにアラートオブジェクトの "title" と "body" フィールドを 設定する別の方法があります 文字列を渡す代わりに ローカライズされた 文字列オブジェクトとして 設定することができます "loc-key" フィールドにはアプリの ローカライゼーション ファイルで見つけられる ローカライゼーションキーを指定します "loc-args" フィールドは ローカライズされた文字列に挿入される 値のリストになります これでデバイスはユーザの 地域に応じて通知を 自動的にローカライズします アラートに最後のタッチを加えるために カスタムサウンドを 追加したいと思います そのためにはまずサウンドファイルを リソースとしてアプリのターゲットに 追加する必要があり 次にアラートオブジェクトの "sound" フィールドを 私のサウンドのファイル名に設定します これで完成です アラートが見た目もサウンドも 素晴らしくなりました これからはライブアクティビティの ユーザ体験に磨きをかけて 改良を加えていくつもりです アドベンチャーが終わったら ライブアクティビティを終了させ 一定時間後に解散させたいです イベントを "end" に設定した プッシュペイロードを送信して行います ライブアクティビティをロック画面から 削除するタイミングを制御したいので カスタムの "dismissal-date" を設定します このフィールドを省くことで ライブアクティビティを 終了させるタイミングを システムに任せることができます "dismissal-date" の値は1970年からの 経過秒でなければなりません またライブアクティビティに最終的な更新 のための最終ステートを提供します これはオプションで 省略した場合にアクティビティは 前のコンテンツステートを表示し続けます 時々ユーザのデバイス プッシュ通知の 受信に不具合が発生することがあります そしてアドベンチャー ライブアクティビティは未だに 古いヘルス値を 表示しているかもしれないです このようなシナリオでは ライブアクティビティのUIで 不正確な情報が表示される可能性が あることを ユーザに警告したいと思います そのためにペイロードに "stale-date"フィールドを追加します この日付を使用して古くなったというビュー をレンダリングするタイミングを決定します Widget Extensionで宣言された アクティビティ設定から 古いビューを提供することができます ビューがActivityViewContextの isStaleプロパティの値に 反応するようにします 同時に複数のアドベンチャー ライブアクティビティがある場合 ロック画面に正しく表示したいです より重要なアップデートがあるものは トップに近く 最も重要なものはDynamic Islandに あるはずです オプションの関連性スコアフィールドを 提供することでこれをアレンジできます 数字が大きいほど関連性が高いことを 示します ライブアクティビティを更新する方法が お分かりいただけたと思います アプリにプッシュ通知を追加しましょう 最初にすべきことは サーバーとアプリの設定です ActivityKitのプッシュ通知に対応させます ターミナルからプッシュアップデートを 送信するテストをして素早くイテレートし それで満足したら エンドツーエンドサポートの 導入を開始します 一方ユーザ体験を念頭に置くべき 適切なプライオリティを使用し 必要に応じてユーザにアラートをします 一緒にライブアクティビティについて 楽しく学んでいただけたなら幸いです みなさんがDynamic Islandと ロック画面に持ち込まれる クリエイティブなアイデアを 見るのが待ち遠しいです
ご視聴ありがとうございました ♪ ♪
-
-
3:53 - Enabling push updates
func startActivity(hero: EmojiRanger) throws { let adventure = AdventureAttributes(hero: hero) let initialState = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "Adventure has begun!" ) let activity = try Activity.request( attributes: adventure, content: .init(state: initialState, staleDate: nil), pushType: .token ) Task { for await pushToken in activity.pushTokenUpdates { let pushTokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) } Logger().log("New push token: \(pushTokenString)") try await self.sendPushToken(hero: hero, pushTokenString: pushTokenString) } } }
-
6:54 - APNs push payload: Updating
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.941, "eventDescription": "Power Panda found a sword!" } } }
-
7:37 - Printing content state JSON
let contentState = AdventureAttributes.ContentState( currentHealthLevel: 0.941, eventDescription: "Power Panda found a sword!" ) let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let json = try! encoder.encode(contentState) Logger().log("\(String(data: json, encoding: .utf8)!)")
-
9:18 - Terminal: Constructing an APNs request with curl
curl \ --header "apns-topic: com.example.apple-samplecode.Emoji-Rangers.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": { "currentHealthLevel": 0.941, "eventDescription": "Power Panda found a sword!" } } }' \ --http2 https://api.sandbox.push.apple.com/3/device/$ACTIVITY_PUSH_TOKEN
-
14:21 - APNs push payload: Alerting
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.0, "eventDescription": "Power Panda has been knocked down!" }, "alert": { "title": "Power Panda is knocked down!", "body": "Use a potion to heal Power Panda!", "sound": "default" } } }
-
14:56 - APNs push payload: Alert localization
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.0, "eventDescription": "Power Panda has been knocked down!" }, "alert": { "title": { "loc-key": "%@ is knocked down!", "loc-args": ["Power Panda"] }, "body": { "loc-key": "Use a potion to heal %@!", "loc-args": ["Power Panda"] }, "sound": "HeroDown.mp4" } } }
-
15:25 - APNs push payload: Alert sound
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.0, "eventDescription": "Power Panda has been knocked down!" }, "alert": { "title": { "loc-key": "%@ is knocked down!", "loc-args": ["Power Panda"] }, "body": { "loc-key": "Use a potion to heal %@!", "loc-args": ["Power Panda"] }, "sound": "HeroDown.mp4" } } }
-
15:52 - APNs push payload: Dismissal
{ "aps": { "timestamp": 1685952000, "event": "end", "dismissal-date": 1685959200, "content-state": { "currentHealthLevel": 0.23, "eventDescription": "Adventure over! Power Panda is taking a nap." } } }
-
16:44 - APNs push payload: Stale date
{ "aps": { "timestamp": 1685952000, "event": "update", "stale-date": 1685959200, "content-state": { "currentHealthLevel": 0.79, "eventDescription": "Egghead is in the woods and lost connection." } } }
-
16:54 - Displaying a stale Live Activity UI
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in AdventureLiveActivityView( hero: context.attributes.hero, isStale: context.isStale, contentState: context.state ) .activityBackgroundTint(Color.gameWidgetBackground) } dynamicIsland: { context in // ... } } }
-
17:19 - APNs push payload: Relevance score
{ "aps": { "timestamp": 1685952000, "event": "update", "relevance-score": 100, "content-state": { "currentHealthLevel": 0.941, "eventDescription": "Power Panda found a sword!" } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。