ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
ウィジェットに命を吹き込む方法
ご自身のアプリやゲームのウィジェットにアニメーションを加えたり、インタラクティブなウィジェットを作成する方法について確認しましょう。エントリのトランジションのアニメーションを調整したり、SwiftUI ButtonとToggleを使ってインタラクティブ性を追加する方法を紹介します。これにより、ホーム画面やロック画面から直接パワフルな体験を提供することができます。
関連する章
- 1:23 - Animations
- 7:45 - Interactivity
リソース
関連ビデオ
WWDC23
- ウィジェットの新しい場所への展開方法
- プッシュ通知によるライブアクティビティの更新
- ActivityKitについて
- App Intentにおける機能強化の詳細
- Xcode PreviewsによるプログラマティックなUIの構築
WWDC22
-
ダウンロード
♪ ♪
Luca:こんにちは Lucaです SwiftUI Teamのエンジニアです 本日は 新しいエキサイティングな 機能を使って ウィジェットに 命を吹き込む方法について説明します ウィジェットは iOSとmacOSの ユーザー体験において 愛されている機能ですが インタラクティブ機能と アニメーションによって 体験がさらに強化されました
インタラクティブ機能により ユーザーは ウィジェットのデータを直接操作でき アプリで最も重要なアクションを実行する 強力なインタラクションを作成できます また アニメーションは ウィジェットに命を吹き込み コンテンツの変化やユーザーの アクションの結果を伝えてくれます 新機能にとてもワクワクしています さっそく始めましょう まず アニメーションと ウィジェットの見た目を良くする 簡単な方法を説明します その後 ウィジェットにインタラクティブ 機能を追加する方法を説明します アニメーションから始めましょう この動画では 友人のNilsが 日中のカフェイン摂取量を 記録するために開発したアプリを使います カフェインの総量と 最後に飲んだ飲み物を表示する ウィジェットがすでにあります ウィジェットを 最新のSDKでコンパイルすると ウィジェットのコンテンツが変化するたび システムはデフォルトのアニメーションで エントリ間の変化を アニメーション化します さらに見た目を良くするために いくつかの調整を加える予定ですが Xcodeを見る前に ウィジェットでアニメーションが どのように機能するかについて 簡単にお話します 通常のSwiftUIアプリでは ビューに変更を発生させるため ステートを使います そしてアニメーションは withAnimationのような モディファイアを使用したステートの 変化によって発生します しかし ウィジェットの 仕組みは少し異なります ウィジェットはステートを持ちません 代わりにエントリで構成される タイムラインを作成し 特定の時間にレンダリングされた 異なるビューに対応します SwiftUIはエントリ間の差異を判別し 変更がある部分をアニメーション化します デフォルトではウィジェットは implicitスプリングアニメーションと 様々なimplicitコンテンツ トランジションを受け取りますが SwiftUIが提供する すべてのトランジション アニメーション コンテンツ トランジションAPIを使用して ウィジェットのアニメーションを カスタマイズできます すべてのSwiftUIのアニメーション プリミティブの仕組みに関しては 「SwiftUIアニメーションの詳細”という 素晴らしいトークがあります ご覧ください では Xcodeを開いてみましょう 少し手を加えるだけで ウィジェットがカプチーノの ラテアートのようにキレイになりますよ 新しいXcode Preview APIでの アニメーションの 作業の様子をお見せします
これが私のウィジェットを構成する すべてのビューです メインビューには 2つのビューを持つVStackがあります 1つ目はカフェインの総量を表示し 2つ目は 今日最後に飲んだ 飲み物があればそれを表示します ウィジェットの背景を定義するために containerBackgroundモディファイアを 使っています これにより MacとiPadで新たに サポートされた場所に表示可能になります 通常 ウィジェットの アニメーションを見るには たくさんのエントリを用意し 画面に表示されるのを待つ必要があります しかし それは手間と時間が掛かります 幸い 今年導入される新しい Preview APIに 素晴らしい解決策があります systemSmallで ウィジェットの 新しいプレビューを定義し ウィジェットを定義する タイプを渡すことができます そして 先に定義したエントリを含む タイムラインの レンダリング方法を指定できます キャンバスでそれを行うと タイムラインのプレビューが表示され すべてのエントリを見ることができます ご覧ください プレビューをクリックすると ウィジェットがエントリ間の トランジションをアニメーションしている 様子が分かります これは便利です ですがこれはまだ 新しいPreview API機能の ほんの一部です この新しい強力なAPIについて さらに詳しく知りたい方は 「Xcode Previewsによるプログラマティックな UIの構築」をご確認ください では アニメーションを調整してみましょう まずは カフェイン量の テキストから始めます 現在は 次の値と クロスフェードしているだけですが 値が上がる様子を ドラマチックに演出してみます この場合 ビューは変化せず テキストコンテンツだけ変化します これをアニメーションさせるには コンテンツトランジションを使用します そして テキストに カフェインの数値を追加します このコンテンツトランジションは 数値が変化した際に 目立たせることで 数値の重要性を 強調するために作成してあります 見た目もよさそうですね では 飲み物を表示するビューに注目します 新たに飲み物を飲んだことを 強調するトランジションを追加します 最初に IDモディファイアを使って このビューのIDとレンダリングする 特定のログを関連付けます このログが変化するたびに SwiftUIに対して これは新しいビューであり トランジションが必要だと通知します これでトランジションを指定できます プッシュにしてみましょう どちら側から? 下からがいいと思います これで手順は分かりましたね プレビューキャンバスに戻りましょう
下からのトランジションはいい感じです 最後にもう一つ調整しましょう コーヒーをたくさん飲むと 神経が高ぶるので それをトランジションの アニメーションカーブに反映させます これは通常のSwiftUIアプリなので アニメーションモディファイアを使用して スムーズなスプリングを選択し 継続時間を短くします そのアニメーションを ログの値にバインドします これでアニメーションが カフェイン摂取量にマッチします 内容には満足したので インタラクティブ機能に目を向けてみます インタラクティブ機能を使って ウィジェットからアクションを実行できます Xcodeで見る前に ウィジェットが動作する アーキテクチャについて少し説明します これにより インタラクティブ機能の より良いメンタルモデルを作れます ウィジェットを作成する際は Widget Extensionを定義します それをシステムが検知して 独立したプロセスとして実行します ウィジェットは 一連のエントリを返す タイムラインプロバイダを定義します これは実質的にウィジェットのモデルです ウィジェットが表示されている場合 システムはWidget Extension プロセスを起動し タイムラインプロバイダに エントリを要求します これらのエントリは ウィジェットの一部を構成する ビュービルダーにフィードバックされ エントリに基づく一連のビューを 生成するために使用されます その後 システムは これらのビューの表示を生成し ディスクにアーカイブします 特定のエントリを表示する時 システムはアーカイブされた ウィジェットの表示をデコードし レンダリングします ここでいったん立ち止まり 最後のポイントをもう一度説明します ビューのコードは アーカイブの間だけ実行されます そのビューの別個の表現は システムプロセスがレンダリングします しかし データが静的でない場合 エントリを更新したい時もあるでしょう これは ウィジェットに表示される データが更新されるたびに アプリでreloadTimelines関数を 呼び出すことで可能になります これにより 先ほど説明した プロセスが繰り返され 新しいエントリが再生成され ビューの コピーがディスクにアーカイブされます このアーキテクチャには 3つの重要なポイントがあります まず ウィジェットが表示されている時 コードは実行されていません タイムラインエントリを更新することで ウィジェットのコンテンツが変更されます これは インタラクティブな ウィジェットにも当てはまります 通常 ウィジェットの更新は ベストエフォートベースで行われますが インタラクションから開始されたリロードは 常に発生すると 保証されていることが重要な点です では インタラクティブ機能を 追加する方法を見て行きましょう ウィジェットの一部を インタラクティブにするために ButtonやToggleのような 使い慣れた コントロールを使うことができます ただし ウィジェットは別のプロセスで レンダリングされるので SwiftUIはプロセス空間で クロージャを実行せず バインドは 変更されないことに注意が必要です そのため Widget Extension によって実行され システムに呼び出されるアクションを 表現する方法が必要です 幸いなことに App Intentという 解決策がすでにあります アプリのアクションをショートカットやSiriに 公開するために 使った方もいるでしょう そして 同じインテントを使用して ウィジェットのアクションを表現できます その核となる AppIntentは システムによって実行されるアクションを コードで定義できるプロトコルです たとえば ここではTodoアプリのTodo項目を 切り替えるAppIntentを定義しています インテントでは いくつかの パラメータを入力として定義し performと呼ばれる非同期関数を定義し インテントを実行するための ビジネスロジックを定義します App Intentはとても強力で 説明したいことがたくさんあります WWDC22と23の 「Appインテントの詳細」と 「App Intentにおける機能強化の詳細」 セッションを ぜひご確認ください SwiftUIとAppIntentsの 両方をインポートする際 UIからApp Intentを 実行する機能をサポートするため ButtonとToggleという 新しいイニシャライザが 引数としてAppIntentを受けとり これらのコントロールと やり取りする時に そのインテントを実行します インタラクティブウィジェットでは AppIntentを使用した ButtonとToggleのみがサポートされています 他のコントロールは動作しません もちろん これらのイニシャライザは アプリでも動作します ウィジェットとアプリの間で AppIntentロジックを 共有できるのが良い点です Xcodeの コーヒートラッカーアプリに戻り インタラクティブ機能を追加してみましょう 現在 ユーザーはアプリを開いた時だけ 新しい飲み物を記録できますが インタラクティブウィジェットは アプリの中で最も重要な アクションを前面に出す 促進剤として機能します このアプリの場合は もちろん 新しい飲み物を記録する機能です すでに作成したファイルに 追加してみましょう 最初にやりたいことは 新しい飲み物を ログに記録する AppIntentに準拠する タイプを定義することです 人間が読みやすいタイトルをつけ システムで使用します そしてエスプレッソをストアに記録し 空のintent resultを返すことで 実行要件を実装します ここで強調しておきたいことは performは非同期関数なので 非同期処理 たとえばここでログ 書き込みを待っているときに データベースへの書き込みなどを行う場合は この関数をフルに活用して いただきたいということです performが返ってくると システムは直ちにウィジェットの タイムラインのリロードを開始し ウィジェットのコンテンツを 更新する機会が与えられます そのため performから戻る前に 更新されたウィジェットを 再読み込みするために必要な すべての情報を永続化しておきましょう 飲み物はエスプレッソと ハードコードしましたが もちろん 特定の飲み物を ログに渡せるようにしたいです そのためには @Parameterプロパティラッパーと パラメータを入力するイニシャライザを持つ ストアドプロパティを追加します このプロパティラッパーを使うことが 重要です なぜなら このプロパティラッパーでアノテートされた ストアドプロパティのみが永続化され Widget Extensionでインテントが 実行された時に利用可能になるからです このインテントを呼び出す ボタンを追加する前に ここでApp Intentを使用する エコシステムの重要な利点を説明します 先ほど定義したアプリのインテントは ショートカットとSiriで利用可能になるため ここで定義しておくことで ウィジェット以外の ユーザー体験にも還元されるのです これで ウィジェットに ボタンを追加する準備ができました 新しいビューを作成して ボタンを追加しましょう このビューでは AppIntentを受け取る ボタンイニシャライザを使い 先ほど定義したインテントを渡します Spacerを使ってウィジェットの 残りの部分にビューを追加します すべて完了したので ウィジェット上で どのように動作するか ビルドして実行しましょう Widget Extensionのターゲットを 直接ビルドすれば Xcodeがウィジェットをホーム画面に インストールしてくれます ウィジェットに 定義したボタンが実装されました タップすると エスプレッソがもう一杯記録されます ですが ウィジェットが可能な限り最高の ユーザー体験を提供できるよう もう1つ変更します AppIntentが動作を終了すると ウィジェットのタイムラインが 再読み込みされます このため アクションから UIが変更されるまで わずかな待ち時間が発生します しかし Mac上のiPhoneウィジェットは この遅延がより 顕著になる可能性があるので それを解決します たとえば このウィジェットでは カフェインの総量を示す値は 更新された エントリが届くまで更新されません invalidatableContentモディファイアで このビューをアノテートできます このウィジェットをiPhoneから Macに追加しました ボタンをタップしてみましょう カフェイン量を示すビューには 更新が届くまで その値が無効であることを示す システムエフェクトが表示されます Buttonの動作と invalidatableContentモディファイアを 使用することで 待ち時間の見え方を 改善する方法を紹介しました このモディファイアは慎重に使用してください 変更の可能性があるすべてのビューに つける必要はありません ユーザーの予想を裏切らない 意味のあるビューに対して このモディファイアを使う方が良いでしょう Toggleはさらに一歩進んで Widget Extensionとの やり取りを待つことなく インタラクションが発生した時に 最適な形でプレゼンテーションを更新します これは 両方の構成でToggleスタイルを プリレンダリングすることで アーカイブ時に自動的に行われます 独自のトグルスタイルを定義する場合は スタイルからisOnプロパティの設定を確認し それを使用して 見た目を切り替えるようにしてください 以上で インタラクティブ機能と アニメーションの解説を終わります アニメーションと インタラクティブ機能によって ウィジェットに 新しい命を吹き込む機会が生まれ ウィジェットが 新しい場所に配置可能になったことで ユーザーがどこにいても 小さな楽しい インタラクションを届けることができます 新しいXcode Preview APIの助けを借りて ウィジェットのアニメーションを調整し アプリの中で最も重要なアクションを見つけ ウィジェットの中で表面化することで ユーザーが必要な時にいつでもどこでも 強力なインタラクションを提供しましょう ありがとうございました ♪ ♪
-
-
3:54 - Usage for the container background modifier
.containerBackground(for: .widget) { Color.cosmicLatte }
-
4:22 - Define a preview for the caffeine tracker widget
#Preview(as: WidgetFamily.systemSmall) { CaffeineTrackerWidget() } timeline: { CaffeineLogEntry.log1 CaffeineLogEntry.log2 CaffeineLogEntry.log3 CaffeineLogEntry.log4 }
-
5:41 - Add a numeric text content transition
struct TotalCaffeineView: View { let totalCaffeine: Measurement<UnitMass> var body: some View { VStack(alignment: .leading) { Text("Total Caffeine") .font(.caption) Text(totalCaffeine.formatted()) .font(.title) .minimumScaleFactor(0.8) .contentTransition(.numericText(value: totalCaffeine.value)) } .foregroundColor(.espresso) .bold() .frame(maxWidth: .infinity, alignment: .leading) } }
-
6:21 - Set up transition on LastDrinkView
struct LastDrinkView: View { let log: CaffeineLog var body: some View { VStack(alignment: .leading) { Text(log.drink.name) .bold() Text("\(log.date, format: Self.dateFormatStyle) · \(caffeineAmount)") } .font(.caption) .id(log) .transition(.push(from: .bottom)) } var caffeineAmount: String { log.drink.caffeine.formatted() } static var dateFormatStyle = Date.FormatStyle( date: .omitted, time: .shortened) }
-
7:18 - Configuring animation for the transition
struct LastDrinkView: View { let log: CaffeineLog var body: some View { VStack(alignment: .leading) { Text(log.drink.name) .bold() Text("\(log.date, format: Self.dateFormatStyle) · \(caffeineAmount)") } .font(.caption) .id(log) .transition(.push(from: .bottom)) .animation(.smooth(duration: 1.8), value: log) } var caffeineAmount: String { log.drink.caffeine.formatted() } static var dateFormatStyle = Date.FormatStyle( date: .omitted, time: .shortened) }
-
9:18 - Reload the timeline for a widget
WidgetCenter.shared.reloadTimelines(ofKind: "LocationForecast")
-
13:06 - App intent to log a caffeine drink
import AppIntents struct LogDrinkIntent: AppIntent { static var title: LocalizedStringResource = "Log a drink" static var description = IntentDescription("Log a drink and its caffeine amount.") @Parameter(title: "Drink", optionsProvider: DrinksOptionsProvider()) var drink: Drink init() {} init(drink: Drink) { self.drink = drink } func perform() async throws -> some IntentResult { await DrinksLogStore.shared.log(drink: drink) return .result() } }
-
15:10 - Create view to log a new drink
struct LogDrinkView: View { var body: some View { Button(intent: LogDrinkIntent(drink: .espresso)) { Label("Espresso", systemImage: "plus") .font(.caption) } .tint(.espresso) } }
-
16:28 - Use the invalidatable content modifier
struct TotalCaffeineView: View { let totalCaffeine: Measurement<UnitMass> var body: some View { VStack(alignment: .leading) { Text("Total Caffeine") .font(.caption) Text(totalCaffeine.formatted()) .font(.title) .minimumScaleFactor(0.8) .contentTransition(.numericText(value: totalCaffeine.value)) .invalidatableContent() } .foregroundColor(.espresso) .bold() .frame(maxWidth: .infinity, alignment: .leading) } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。