ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
ActivityKitについて
ライブアクティビティによって、アプリ内のタスクの進捗を一目で確認できるようになります。ロック画面、Dynamic Island、スタンバイのために役立つ体験を作成する方法について解説します。アプリのライブアクティビティを更新し、アクティビティの状態を監視し、WidgetKitとSwiftUIを活用してリッチな体験を構築する方法をご確認ください。
関連する章
- 0:00 - Intro
- 0:36 - Live Activity overview
- 4:23 - Lifecycle of Live Activities
- 10:43 - Building Live Activity UI
- 16:37 - Wrap-up
リソース
- ActivityKit
- Displaying live data with Live Activities
- Human Interface Guidelines: Live Activities
- Starting and updating Live Activities with ActivityKit push notifications
- WidgetKit
関連ビデオ
WWDC23
-
ダウンロード
♪ ♪
こんにちは Can Aranです iOS System Experience Teamの エンジニアです ライブアクティビティについて お話しできてうれしいです 今回は ライブアクティビティの 概要についてお話しします そして ライブアクティビティの ライフサイクルについて説明します 最後に アクティビティ用のイマーシブな シンプルなUIの構築方法を紹介します まず ライブアクティビティで 何ができるかを説明します ライブアクティビティは イベントやタスクの進捗を 一目で把握できる イマーシブな方法です 開始と終了を個別に設定し バックグラウンドアプリのランタイムから あるいはプッシュ通知を使って リモートから リアルタイムの アップデートを提供できます ユナイテッド航空とMLBの 素晴らしい例をご覧ください iPhone 14 ProとPro Maxでは一層 ライブアクティビティがイマーシブです Dynamic Islandは アプリがバックグラウンドにあるとき システム全体に ライブアクティビティを表示します ライブアクティビティがアクティブになると 可変幅の「コンパクト」 プレゼンテーションでレンダリングされます Dynamic Islandは一度に最大 2つのライブアクティビティを表示します そのうち1つはTrueDepthカメラに 接続されて表示され もう1つは独立したビューで レンダリングされます どちらのライブアクティビティも 「最小限」の表示を使用します ライブアクティビティを長押しすると いつでも「拡張」画面が表示され より多くの情報を一目で確認できます 拡張表示ではアプリ内の様々なエリアに ディープリンクすることができ リッチなユーザーエクスペリエンスを 提供します iOS 17ではライブアクティビティに 関する新しい体験が複数あります ロック画面とDynamic Islandに加えて ライブアクティビティは スタンバイにも表示されます そして iPadもライブアクティビティを サポートするようになりました iPadOSで実装を有効にして Crumbl Cookiesのこのような イマーシブなライブアクティビティを iPadで実現しましょう iOS 17では WidgetKitとSwiftUIを使って ライブアクティビティに インタラクティブ性を追加できます ユーザーエクスペリエンス向上のため ボタンやトグルを追加できます Lucaの動画 「ウィジェットに命を吹き込む方法」で ウィジェットにインタラクティブ性を 持たせる方法の詳細をご確認ください ライブアクティビティは ActivityKitフレームワークに依拠しており アプリがアクティビティのリクエストや更新 ライフサイクル管理をできるようにします それらはSwiftUI と WidgetKitを使って 宣言的にレイアウトされます ホーム画面のウィジェットの実装経験が あれば 非常に馴染み深いでしょう ライブアクティビティはフォアグラウンドに アプリがあるときにリクエストできます アプリは個別のユーザーアクション後にのみ ライブアクティビティをリクエストすべきで 例えば特定のイベントを「フォロー」したり 明示的にタスクを開始したりといった具合です これはポジティブなユーザー体験を 保証するために非常に重要なことです ライブアクティビティは通知と同様 ユーザーによって管理されます 誰かが簡単にあなたのアプリに対して それらの却下やオフができます APIはロック画面から3つの Dynamic Islandプレゼンテーションまで すべてのプレゼンテーションを サポートする必要があります スタンバイではシステムは ロック画面のプレゼンテーションを 画面いっぱいに拡大縮小します バックグラウンドランタイムに 依拠するだけでなく アプリで "liveactivity" プッシュタイプを指定し プッシュ通知を使用したライブ アクティビティのリモート更新も可能です プッシュ通知でライブアクティビティを 更新する方法の詳細は Jeffの動画をご確認ください アプリのライブアクティビティは ライフサイクルの中で様々な段階を経ます 私はEmoji Rangersアプリから ヒーローを選んで 冒険に連れ出す ライブアクティビティを作っています 冒険の間 ヒーローは課題に直面し ボスと戦います この冒険の重要な瞬間を ライブアクティビティに表示します このライブアクティビティには ヒーローの冒険に関する 最も重要な情報が表示されます これにはヒーローの名前と統計 アバター 健康レベル このヒーローが冒険で経験することの 説明が含まれます ライブアクティビティの ライフサイクルは主に4ステップです アクティビティのリクエストから始めます アクティビティが始まったら 最新のコンテンツで更新してください その間 アクティビティを観察し アクティビティが終了するなどの 状態変化に対応しましょう タスクが完了したら アクティビティを終了してください ライブアクティビティの リクエストは非常に簡単です アプリがフォアグラウンドの状態で 初期コンテンツと 必要なアクティビティリクエストデータが あるようにアプリを設定してください Emoji Rangersのアプリで ライブアクティビティをリクエストする前に 「ActivityAttributes」を実装して ライブアクティビティ用の 静的・動的データのセットを 定義することから始めます これを「AdventureAttributes 」と 呼んでいます 「AdventureAttributes」は 1つの静的データを記述します また 必要なカスタム 「ContentState」を定義し ヒーローの健康レベルと イベントの説明をカプセル化します これらのプロパティが変更されると ライブアクティビティUIが更新され 画面上に冒険の現在の状態を 表示できるようになります 動的データと静的データの準備ができたので 冒険アクティビティの リクエストをセットアップします ヒーローでAdventureAttributesの インスタンスを作成し ヒーローの健康レベルとイベントの説明で 初期コンテンツを設定します 各アクティビティコンテンツは コンテンツが古いと判断されたときに システムに通知するために 古くなった日付を提供することができます 今のところは nilを渡します コンテンツの関連性スコアは 複数の冒険アクティビティが開始されたとき 各ライブアクティビティの表示順を決めます もし別の冒険アクティビティを 始めるのであれば それぞれに異なる 関連性スコアを指定するでしょう 関連度スコアの指定は任意です デフォルト値は0です アクティビティを リクエストできるようになりました 属性や初期コンテンツや プッシュ通知タイプを渡します プッシュ通知タイプは ライブアクティビティがActivityKitの プッシュ通知で 動的コンテンツの更新を 受け取るかどうかを示します この例では「nil」に設定します このアクティビティがローカルでのみ 更新を受け取れることを意味します このをライブアクティビティ開始するには Emoji Rangersアプリの ライブアクティビティ設定を 有効にする必要があります ライブアクティビティが リクエスト可能になったので ヒーローがスリル満点の タスクをこなしたときに 冒険を更新する方法を調べます ダイナミック属性はライブアクティビティを 更新するタイミングを教えてくれます イベントの説明やヒーローの 健康レベルが変わるたびに 自分のアクティビティを更新します しまった! ヒーローがボスから致命傷を受けた そこで健康レベルの変化を反映した 「contentState」を作成し イベントを記述します ヒーローの健康レベルが大幅に減少したので アラートを送る必要があります そのためのアラート設定を作ります これによりライブアクティビティで 重要な情報が変化した場合 iPhone iPadまたは同期された Apple Watchにアラートが表示されます この場合 主人公は重傷を負い 回復のためにポーションを必要とする 指定音とともに表示されます 設定タイトルと本文は Apple Watchでのみ使用され 通知として表示されます iPhoneとiPadでは 更新された内容のアクティビティUIが 指定された音と共に表示されます これで更新されたコンテンツと アラート設定を持つ アクティビティオブジェクトの update APIを呼び出すことができます これによりライブアクティビティの UIが更新され ユーザーにアラートが通知されます アクティビティの状態変更は ライブアクティビティのライフサイクル中 いつでも起こり得ます 可能な状態は4つあります 「started」 「finished」 「dismissed」 「stale」です アクティビティオブジェクトの activityStateUpdates APIを使って これらの状態を観察し 非同期で更新を受け取ります アクティビティが却下されたら もう冒険データを 追跡していないことを確認し アプリのUIを更新して進行中の アクティビティを表示しないようにします activityState APIを使って 状態をチェックし 必要なときに同期的に 取得することもできます 僕のヒーローは たくさんのことを経験しました 冒険ライブアクティビティを 終了する時が来ました アクティビティを終了させるために まず最後のコンテンツを作ります 私のコンテンツは主人公がボスを倒した 冒険の最終状態を表示します それからUIの終了ポリシーを決めます この場合は デフォルトのポリシーが適しています このポリシーでは 冒険終了後 しばらくはロックスクリーンに 冒険情報が表示されるので 誰かがロックスクリーンを見て 冒険の最後に何が起こったかを 確認することができます これで冒険アクティビティを終了し ヒーローを休ませることができます ライブアクティビティのライフサイクルに 関するすべてのロジックを構築しました アクティビティUIに集中する番です 現在 Emoji Ranger Widgets Extensionには WidgetBundleに 2つのウィジェットがあります WidgetBundleにライブアクティビティの 設定を追加する必要があります 「AdventureActivityConfiguration」と 呼ぶことにします 「AdventureActivityConfiguration」は ウィジェットインフラストラクチャを 活用するので ボディで WidgetConfigurationを返します ActivityConfigurationオブジェクトを 作成し ライブアクティビティの 内容を記述します 各プレゼンテーションクロージャに対して ActivityConfigurationオブジェクトは 私の静的および動的属性と アクティビティIDを格納する ActivityViewContextを提供します このコンテキストは コンフィギュレーションに渡された 属性タイプに基づいて作成されます このタイプはアクティビティが要求される 属性と一致しなければなりません アクティビティコンフィギュレーションが 正常に初期化されるよう ここでは「AdventureAttributes」 タイプを渡します 「ActivityConfiguration」の 最初のクロージャは ロック画面のUIを指定します アクティビティが更新されると ビューコンテキストが変わるので 更新のたびに このUIがレンダリングされます ウィジェットと同様に ライブアクティビティの ロック画面UIのサイズは指定せず システムに適切なサイズを 決定してもらいます 絵文字レンジャーのアクティビティでは ヒーロー情報や名前とアバター 健康レベルそしてイベントの説明を 紺色の背景のロック画面に表示します 「AdventureLiveActivityView」には 渡されたビューコンテキストを通して すべての情報が表示されます ロック画面のライブアクティビティは シンプルでエレガントに見え ヒーローが冒険の中で 経験することについて 必要な情報をすべて持っています ロック画面のUIを完成させたので 次はDynamic Islandの プレゼンテーションを実装します コンパクトと最小限と拡張の 3つのプレゼンテーションがあります アプリのライブアクティビティだけが システム上で動作しているときは コンパクトなプレゼンテーションを 使って表示されます コンパクトなプレゼンテーションには 先頭と末尾の2つのエリアがあります これらは一緒に表示されDynamic Islandで まとまったプレゼンテーションを形成します スペースは限られているので 先頭と末尾のスペースに表示する 必須コンテンツを選択します ユーザーはここのコンテンツを見て 特別なアクティビティを特定できるはずです ActivityConfigurationオブジェクトの 「DynamicIsland」クロージャで 再びビューコンテキストにアクセスして expanded compactLeading compactTrailing minimalビューを作成します それぞれのプレゼンテーションを 表現するために DynamicIslandビュービルダーを 作成する必要があります ヒーローの冒険では ヒーローのアバターを 先頭側のコンテンツに追加し 健康レベルを 末尾側のビューに追加します また ヒーローの健康レベルに基づいて ダイナミックな色合いを設定します これで冒険のためのコンパクトな プレゼンテーションが出来上がりました 複数のアプリが ライブアクティビティを開始すると システムは表示する ライブアクティビティを選択し それぞれのアクティビティに対して 最小限のプレゼンテーションを使用して 両方を表示します 1つの最小限の プレゼンテーションはDynamic Islandに 接続された状態で表示され もう1つは切り離された状態で表示されます 最小限のビューでは作業できるスペースが 非常に限られているため 最も重要な情報のみ表示する必要があります ライブアクティビティの最小限ビューの場合 最も重要な情報は ヒーローの正体とヒーローの健康状態です よって ユーザーは最小限ビューを見ることで ヒーローを助けるタイミングがわかります これで ユーザーは最小限表示を見れば ヒーローを助けるタイミングを 知ることができます ユーザーがコンパクトまたは 最小限表示のライブアクティビティを タッチおよびホールドすると システムはコンテンツを拡張表示します こちらもサポートする必要があります 拡張プレゼンテーションでは システムは拡張プレゼンテーションを 異なるエリアに分割します DynamicIslandビュービルダーの 最初のクロージャは 展開されたコンテンツを表します そのクロージャの中で 展開された領域が特定の位置を渡すことで 各セクションのコンテンツを 定義することができます ヒーローの名前とアバターを 先頭のスペースに ヒーローの統計情報を末尾のスペースに そして最後に ヘルスバーとイベントの説明を 一番下のスペースに追加します 最終的に 私のDynamic Island UIは シンプルに見え 冒険に必要なすべての情報を提供します さあ 大好きなヒーローと一緒に 冒険の旅に出ましょう! 今作ったばかりのシンプルでイマーシブな ライブアクティビティUIで 一緒に冒険を楽しみましょう 独自のUIをデザインする際 ライブアクティビティには 最も必要なコンテンツだけを表示しましょう シンプルに保ち ユーザーがライブアクティビティを タップすると アプリの追加詳細が表示されます 詳しくは「ダイナミックなライブ アクティビティのデザイン」をご確認ください ライブアクティビティは 進行中のアクティビティを 一目で見られるように ライブで表示する強力なツールです 簡単な設定でiOSやiPadOSのユーザーと ダイナミックに関わることができます プッシュ通知の詳細については 「プッシュ通知によるライブアクティビティ の更新」をご確認ください ActivityKitを使って みなさんが何を開発するのか楽しみです ご視聴ありがとうございました! ♪ ♪
-
-
5:40 - Define ActivityAttributes
import ActivityKit struct AdventureAttributes: ActivityAttributes { let hero: EmojiRanger struct ContentState: Codable & Hashable { let currentHealthLevel: Double let eventDescription: String } }
-
6:28 - Request Live Activity with initial content state
let adventure = AdventureAttributes(hero: hero) let initialState = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "Adventure has begun!" ) let content = ActivityContent(state: initialState, staleDate: nil, relevanceScore: 0.0) let activity = try Activity.request( attributes: adventure, content: content, pushType: nil )
-
8:00 - Update Live Activity with new content
let heroName = activity.attributes.hero.name let contentState = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "\(heroName) has taken a critical hit!" ) var alertConfig = AlertConfiguration( title: "\(heroName) has taken a critical hit!", body: "Open the app and use a potion to heal \(heroName)", sound: .default ) activity.update( ActivityContent<AdventureAttributes.ContentState>( state: contentState, staleDate: nil ), alertConfiguration: alertConfig )
-
9:30 - Observe activity state
// Observe activity state asynchronously func observeActivity(activity: Activity<AdventureAttributes>) { Task { for await activityState in activity.activityStateUpdates { if activityState == .dismissed { self.cleanUpDismissedActivity() } } } } // Observe activity state synchronously let activityState = activity.activityState if activityState == .dismissed { self.cleanUpDismissedActivity() }
-
10:03 - Dismiss Live Activity with final content state
let hero = activity.attributes.hero let finalContent = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "Adventure over! \(hero.name) has defeated the boss! Congrats!" ) let dismissalPolicy: ActivityUIDismissalPolicy = .default activity.end( ActivityContent(state: finalContent, staleDate: nil), dismissalPolicy: dismissalPolicy) }
-
10:50 - Add ActivityConfiguration to WidgetBundle
import WidgetKit import SwiftUI @main struct EmojiRangersWidgetBundle: WidgetBundle { var body: some Widget { EmojiRangerWidget() LeaderboardWidget() AdventureActivityConfiguration() } }
-
11:05 - Define Lock Screen presentation
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.navyBlue) } dynamicIsland: { context in // ... } } }
-
13:28 - Define Dynamic Island compact presentation
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in // ... } dynamicIsland: { context in DynamicIsland { // ... } compactLeading: { Avatar(hero: context.attributes.hero) } compactTrailing: { ProgressView(value: context.state.currentHealthLevel) { Text("\(Int(context.state.currentHealthLevel * 100))") } .progressViewStyle(.circular) .tint(context.state.currentHealthLevel <= 0.2 ? Color.red : Color.green) } minimal: { // ... } } } }
-
14:42 - Define Dynamic Island minimal presentation
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in // ... } dynamicIsland: { context in DynamicIsland { // ... } compactLeading: { // ... } compactTrailing: { // ... } minimal: { ProgressView(value: context.state.currentHealthLevel) { Avatar(hero: context.attributes.hero) } .progressViewStyle(.circular) .tint(context.state.currentHealthLevel <= 0.2 ? Color.red : Color.green) } } } }
-
15:26 - Define Dynamic Island expanded presentation
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in // ... } dynamicIsland: { context in DynamicIsland { // Leading region DynamicIslandExpandedRegion(.leading) { LiveActivityAvatarView(hero: hero) } // Expanded region DynamicIslandExpandedRegion(.trailing) { StatsView(hero: hero, isStale: isStale) } // Bottom region DynamicIslandExpandedRegion(.bottom) { HealthBar(currentHealthLevel: contentState.currentHealthLevel) EventDescriptionView(hero: hero, contentState: contentState) } } compactLeading: { // ... } compactTrailing: { // ... } minimal: { // ... } } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。