ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
システム全体にアプリのコントロールを拡張
アプリのコントロールをコントロールセンターやロック画面などに配置する方法を確認しましょう。WidgetKitを使用して、アプリのコントロールをシステムの利用体験に拡張する方法を解説します。コントロールを作成し、デザインをカスタマイズしたり、構成を変更したりする方法も取り上げます。
関連する章
- 0:00 - Introduction
- 0:37 - Learn about controls
- 3:04 - Build a control
- 6:39 - Update toggle states
- 12:25 - Make controls configurable
- 14:40 - Add refinements
リソース
- Adding refinements and configuration to controls
- Creating a camera experience for the Lock Screen
- Creating controls to perform actions across the system
- Forum: App & System Services
- Human Interface Guidelines: Controls
- Updating controls locally and remotely
関連ビデオ
WWDC24
-
ダウンロード
こんにちは Cliffです System Experienceチームで エンジニアをしています 今日は iOS 18の 新しいウィジェットタイプである コントロールの作成方法について お話しします まず コントロールの概要と ユーザーがどのように使用するかを説明します 次に コントロールの作成方法を紹介し アクションを実行して その状態を維持する方法 構成をサポートする方法 システムとの統合を改善する方法を 説明します
コントロールは アプリの機能を システムスペースに拡張する新しい方法です システムスペースには コントロールセンター ロック画面 アクションボタンなどがあります これらのコントロールは WidgetKitを用いて作成されます iOS 14でWidgetKitが導入され 視覚に訴える カスタムスタイルのコンテンツを 詳細情報とともに アプリに表示できるようになりました ウィジェットを利用すれば 天気や次のカレンダーイベントを 効果的に表示できます iOS 18ではコントロールが追加され WidgetKitをさらに拡張しています コントロールはアプリからのアクションに 迅速にアクセスできる優れた方法です これらのコントロールはアクションと 簡潔な情報にフォーカスしており フラッシュライトをオンにしたり 時計アプリにディープリンクするなどの ユースケースに適しています ウィジェットの作成方法を知っていれば コントロールも同じように作成できます 基盤となるアーキテクチャは 同じものだからです コントロールには2種類あります ボタンとトグルです ボタンは アプリの起動など 個別のアクションを実行します トグルはブール値の状態を変化させます 何かのオンとオフを切り替える場合などです
インタラクティブなウィジェットのように コントロールはApp Intentを使って アクションを実行します
基本的に コントロールは システムスペースに 目にみえる形で現れるアクションで アプリから提供される情報が使用されます
アプリは シンボル タイトル 色合い 追加のコンテンツを システムに提供します サポートされているシステムスペースに ユーザーがコントロールを追加すると そのコントロールが状況に応じて システムスペースに表示されます
コントロールセンターでは コントロールが 3つの異なるサイズで表示されるため タイトルや値のテキストが 常に表示されるとは限りません 私は仕事中に 集中力を維持しながら 休憩を取りたい時に よくタイマーを使います 今日は 作業と休憩を区切って実行できる 生産性タイマーコントロールを作成します タイマーが作動中 ライブアクティビティで 残り時間が表示されます
ロック画面からタイマーを開始し
コントロールセンターで停止し
さらにアクションボタンを使って 開始または停止することができます
生産性を高めるこのタイマーコントロールを ゼロから構築してみます まずは基本的なトグルから始めて その後 コントロールの様々な機能を 活用していきます
既存の 生産性ウィジェット用の WidgetBundleが既にあるので まず このWidgetBundleに TimerToggle()エントリを追加します 次に この同じWidget Extensionで TimerToggle()コントロールを定義します ControlWidgetに準拠した TimerToggleタイプを追加します コントロールを定義するため コントロールに表示する情報と 実行するアクションを提供します
まずは StaticControlConfiguration から始めます このコントロールは構成可能ではありません 構成は後で追加します
このコントロールは 一意の識別子としてkindを取り コントロールのタイプの定義として ControlWidgetToggleを取ります
次に コントロールにタイトルを与えて その状態を提供します
コントロールが操作されたときに 実行するアクションも記述します インタラクティブなウィジェットと同様 このコントロールもApp Intentを 使用してアクションを実行します
最後に コントロールを定義する シンボル画像を指定します
必要な情報がすべて揃ったので このコントロールがシステムに 表示されるようになります
コントロールセンターにも配置して タイトル シンボル オン/オフ状態を表示できます
このタイマーコントロールは タイマーが作動中または停止中に 異なるシンボルを表示することで さらに改善できます
そのためには クロージャのisOn引数を使用し コントロールがオンのときは 砂時計シンボルを表示して 時間がカウントダウンされていることを示します
素晴らしいですね! コントロールが作動中のときのみ 砂時計が表示されるようになりました
また この状態の値のテキストも さらに改善したいと思います 現在はオンとオフが表示されています
これはトグルのデフォルトの値のテキストです しかし通常 タイマーでは オン/オフではなく 作動中/停止を使用します
コントロールの値のテキストは 画像をラベルに変更してカスタマイズできます ラベルには値textと systemImageの両方が含まれます これでデフォルトのオン/オフではなく コントロールの状態を表す適切で関連性のある 値のテキストが表示されるようになりました
この値のテキストは コントロールがロック画面上にあるときや コントロールセンターの小さなサイズ内では 表示されないことに注意してください その場合は シンボルのみが表示されます
このコントロールの方向性は 気に入っていますが カラーについては オン状態ではシンボルが デフォルトのsystemBlueの色合いなので 私の生産性アプリの ブランドイメージに合いません
デフォルトのsystemBlueではなく 特徴的な色をコントロールで使用するには 色合いを指定します
生産性アプリの色合いである パープルを使用します パープルは生産性を高めます 色合いはトグルがオンのときに シンボルに使用されます
こちらは ロック画面上で動作する スタイルが適用されたコントロールと アクションボタンの例です コントロールセンターで動作させるものと 同じコードを使用しています コントロールがオンのとき シンボルと任意の値のテキストが 指定した色合いになります トグルがどのように状態を表示し 管理するかを調べてみましょう ここまで TimerManagerクラスを使って 現在のタイマーの状態を コントロールに提供してきています この例では TimerManagerは 私の 生産性アプリと 同じデータにアクセスする 共有グループコンテナでデータを確認し 実行状態を同期的に取得します コントロールの状態やコンテンツが 変更されたときに システムがコントロールをどのように リロードするのかを見ていきましょう
システムがコントロールを リロードする必要があるとき Widget Extensionプロセス内で コントロールの本体を実行して 現在の値を取得し コントロールのコンテンツを生成します コントロールの値とコンテンツは システムに渡され コントロールの表示に使用されます つまり Widget Extensionは コントロールの現在の状態と その状態のコンテンツを提供します
システムがコントロールをリロードする 原因となるイベントは3種類あります コントロールアクションが実行された時
アプリがオンデマンドで コントロールのリロードを要求した時 プッシュ通知がコントロールを 無効にした時です 最初のイベントは コントロールアクションの実行時です ユーザーがコントロールを操作するたびに コントロールのApp Intentの perform()関数が返される時点で コントロールが自動的にリロードされます リターンされる前に すべての更新を完了させてください
このタイマーコントロールでは アクションはToggleTimerIntent()です
このインテントは タイマーの「作動中」状態を設定し ライブアクティビティを開始または停止します
このApp Intentはタイマーの「作動中」状態を システムが提供する値に設定するため SetValueIntentになります タイマーのライブアクティビティを変更するため LiveActivityIntentでもあります perform関数が完了すると システムは新しい状態で コントロールを更新します タイマーコントロールを操作することが 状態を変更する 唯一の方法ではありません 生産性アプリを開き そこでタイマーを 開始/停止することもできます コントロールを最新の状態に 維持したいのですが
そのためには タイマーの状態が変化したとき ControlCenter APIを使って コントロールをリフレッシュします タイマーコントロールの種類を指定して リロードするコントロールを指定するのです これで アプリでタイマーを開始すると コントロールの状態が常に 最新の状態に保たれます
コントロールの状態やコンテンツを リフレッシュする必要がある場合は アプリからシステムに コントロールのリロードを要求できます ウィジェットやライブアクティビティに利用できる リフレッシュツールは コントロールにも利用できます コントロールを開発し その状態を頻繁にリフレッシュする際は で を有効にして システムポリシーを コントロールから削除します このデバイスでは 生産性タイマーが うまく作動しています これを複数のデバイスで作動させ すべてのデバイスが サーバ上の同じタイマー状態に アクセスできるようにしたいと思います
このコントロールは デバイスで利用できない サーバの状態を反映するため タイマーの状態を非同期で 取得する必要があります そのためには ValueProviderを使用できます
ControlValueProviderには 2つの要件があります currentValue()とpreviewValueです
currentValue()は非同期であり 必要なデータを取得する際は データベースやサーバから データを取得できます この場合 TimerManagerは サーバから非同期でタイマー状態をクエリします
状態を処理できなかった場合は それを知らせるエラーをスローし 後でコントロールをリロードする 必要があることを通知できます
previewValueでは コントロールが追加される前 ユーザーがプレビューしたときに 表示する値を選択します 例えば コントロールギャラリー ロック画面のカスタマイズ時 アクションボタンの設定などで使用します previewValueは事前に決定しておき 極めて短時間で戻る必要があります また コントロールのオフ状態に 対応する値を使用する必要があります
ValueProviderを取る 別のコントロールイニシャライザで このValueProviderを使用でき 提供された値がトグルを定義する クロージャに渡されます ここでは その値を コントロールのisOn状態として使用します この例では 単純なブールを値として 使用していますが 値にさらに多くの 情報を含めることもできます その例は後で紹介します システムがValueProviderを使用して コントロールをリロードするとき 最初にValueProviderを実行して 現在の値を取得し その値をコントロールクロージャに渡して コンテンツを生成します これらすべては Widget Extensionプロセスで行われます
これで 生産性タイマーの状態が サーバに保存され 様々なデバイスから変更できます 例えば iPadから タイマーを開始または停止した場合 このデバイス外の状態変更が 他のデバイス上のコントロールの リロードをトリガするようにしたいとします その場合は Push Notification APIを使用して コントロールのpushハンドラを定義し 外部状態変更の プッシュ通知を受信したときに コントロールがリロードされるように 構成します プッシュ通知の取り扱いに関する ドキュメントには これを行う方法とベストプラクティスの 詳細が記載されています
これで iPadでタイマーを停止すると iPhone上のコントロールも 停止するようになりました
私の 生産性タイマーは 順調に動作していますが 仕事とプライベートで 別々のタイマーを用意したいと思います 例えば バイオリンの練習などです アプリでそれぞれを個別に 追跡できるようにしたいと思います それぞれのタイマー用に 異なるコントロールを用意し 両方をコントロールセンターに 配置できるといいですね WidgetKitを使用して ウィジェットと 同じようにコントロールを作成できるため コントロールを構成可能にすることで これを実現できます コントロールセンターに タイマーコントロールを1つ追加した後 仕事用またはプライベート用の どちらを開始/停止するかを 選択できるようにしましょう それで コントロールセンターに仕事用と プライベート用のタイマーコントロールを それぞれ配置できます まず ValueProviderを新しいプロトコルに 準拠するように更新できます AppIntentControlValueProviderです これで 値がインテントの設定に 依存するようになります 構成を決めるApp Intentは SelectTimerIntentです コントロールを操作するタイマーを ユーザーが選択できます 構成の特定のタイマーの 作動状態が確実に取得されるようにして 返す値はタイマーとその作動状態を含む カスタム構造体になります
ConfigurableValueProviderを使用して AppIntentControlConfiguration()で コントロールを構成可能にします このクロージャに渡される値は timerState構造体です トグルを完了するために そのタイマーと作動状態を使用します 特定のタイマーの名前をコントロールの タイトルとして表示したいと思います また トグルタイマーのApp Intentは その特定のタイマーに対して動作します
次に ユーザーがコントロールセンターを カスタマイズするとき このコントロールで どのタイマーを操作するか 選べるようにします ここでは コントロールセンターに 並べて配置した 仕事用とプライベート用の タイマーコントロールが 各タイマーをコントロールしています
コントロールを機能させるために 構成が必要な場合 promptsForUserConfiguration() モディファイアを使用して システムがコントロールを システムスペースに追加した際に 構成を促すプロンプトを 自動的に表示するようにできます
コントロールをさらに洗練させて システムのデフォルト値が ユースケースに合わない場合に 最も理解しやすく関連性のある コンテンツを提供できます 例えば アクションが実行される前に ユーザーがアクションボタンを操作すると アクションのヒントが表示されます 現在のアクションヒントは とです
なぜそうなっているのかを 詳しく見てみましょう
コントロールの値ラベルをか にカスタマイズする前は デフォルトのオン/オフの 値のテキストの生成と同じように と という アクションヒントが生成されていました 値のテキストをカスタマイズした際 それがアクションヒントの生成にも使われました システムでと が生成されました これらは確実に改善できます そこで これをユースケースに合わせて カスタマイズします
controlWidgetActionHintモディファイアを 使用して アクションボタンのヒントテキストを カスタマイズし アクションヒントを選びます これは動詞で始める必要があります 提供されたヒントは 特定の状態に遷移するアクションです タイマーがオン状態での アクションヒントは つまり タイマーを開始するヒントは です オフ状態のアクションヒントは つまり タイマーを停止するヒントは です 素晴らしいですね タイマーとして自然に感じられます
controlWidgetStatusモディファイアを使用して アクションが実行されたとき コントロールセンターに 一時的なステータスを表示できます コントロールのアクションに関する追加情報 その状態 または その状態の有効期間を伝える場合 ステータスの追加をお勧めします ステータステキストは慎重に使用し 注意を引くために コントロールでまだ伝えていない 関連情報にのみ使用してください
コントロールギャラリーから コントロールを追加すると 現在の名前は これは私のアプリの名前です
コントロールのアプリ名が コントロールのデフォルト表示名です
コントロールの表示名を にカスタマイズします 各コントロールに必ず特定の displayNameを選択してください 機能に応じて異なります 最後の仕上げとして 説明も追加します コントロールの構成中に 表示されるものです わずか数ステップで コントロールセンター ロック画面 アクションボタンに配置でき すべてのデバイスで同期する 生産性タイマーコントロールを作成しました コントロールはパワフルな機能です iOSおよびiPadOS 18の アプリに組み込むことで システムスペースでアプリの主要アクションに 素早くアクセスすることができます ここで説明したモディファイアを使用して コントロールのスタイルを アクションに合わせて調整し コントロールに特徴的なシンボルが 備わっていることを確認してください
カメラでコンテンツをキャプチャできる コントロールを作成する場合は キャプチャ拡張の作成に関するセッション 「Build a great Lock Screen camera capture experience」をご覧ください
ご視聴ありがとうございました
-
-
3:13 - Add the control to the Widget Bundle
@main struct ProductivityExtensionBundle: WidgetBundle { var body: some Widget { ChecklistWidget() TaskCounterWidget() TimerToggle() } }
-
3:29 - Complete the control
struct TimerToggle: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration( kind: "com.apple.Productivity.TimerToggle" ) { ControlWidgetToggle( "Work Timer", isOn: TimerManager.shared.isRunning, action: ToggleTimerIntent() ) { _ in Image(systemName: "hourglass.bottomhalf.filled") } } } }
-
4:41 - Specify different symbols when on and off
struct TimerToggle: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration( kind: "com.apple.Productivity.TimerToggle" ) { ControlWidgetToggle( "Work Timer", isOn: TimerManager.shared.isRunning, action: ToggleTimerIntent() ) { isOn in Image(systemName: isOn ? "hourglass" : "hourglass.bottomhalf.filled") } } } }
-
5:21 - Specify custom value text and add a custom tint color
struct TimerToggle: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration( kind: "com.apple.Productivity.TimerToggle" ) { ControlWidgetToggle( "Work Timer", isOn: TimerManager.shared.isRunning, action: ToggleTimerIntent() ) { isOn in Label(isOn ? "Running" : "Stopped", systemImage: isOn ? "hourglass" : "hourglass.bottomhalf.filled") } .tint(.purple) } } }
-
8:14 - Implement timer toggling
struct ToggleTimerIntent: SetValueIntent, LiveActivityIntent { static let title: LocalizedStringResource = "Productivity Timer" @Parameter(title: "Running") var value: Bool // The timer’s running state func perform() throws -> some IntentResult { TimerManager.shared.setTimerRunning(value) return .result() } }
-
8:54 - Refresh the control from within the app
func timerManager(_ manager: TimerManager, timerDidChange timer: ProductivityTimer) { ControlCenter.shared.reloadControls( ofKind: "com.apple.Productivity.TimerToggle" ) }
-
10:03 - Define a Value Provider
struct TimerValueProvider: ControlValueProvider { func currentValue() async throws -> Bool { try await TimerManager.shared.fetchRunningState() } let previewValue: Bool = false }
-
11:00 - Provide asynchronously fetched state with a Value Provider
struct TimerToggle: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration( kind: "com.apple.Productivity.TimerToggle", provider: TimerValueProvider() ) { isRunning in ControlWidgetToggle( "Work Timer", isOn: isRunning, action: ToggleTimerIntent() ) { isOn in Label(isOn ? "Running" : "Stopped", systemImage: isOn ? "hourglass" : "hourglass.bottomhalf.filled") } .tint(.purple) } } }
-
13:06 - Make the Value Provider configurable
struct ConfigurableTimerValueProvider: AppIntentControlValueProvider { func currentValue(configuration: SelectTimerIntent) async throws -> TimerState { let timer = configuration.timer let isRunning = try await TimerManager.shared.fetchTimerRunning(timer: timer) return TimerState(timer: timer, isRunning: isRunning) } func previewValue(configuration: SelectTimerIntent) -> TimerState { return TimerState(timer: configuration.timer, isRunning: false) } }
-
13:40 - Make the timer configurable
struct TimerToggle: ControlWidget { var body: some ControlWidgetConfiguration { AppIntentControlConfiguration( kind: "com.apple.Productivity.TimerToggle", provider: ConfigurableTimerValueProvider() ) { timerState in ControlWidgetToggle( timerState.timer.name, isOn: timerState.isRunning, action: ToggleTimerIntent(timer: timerState.timer) ) { isOn in Label(isOn ? "Running" : "Stopped", systemImage: isOn ? "hourglass" : "hourglass.bottomhalf.filled") } .tint(.purple) } } }
-
14:26 - Prompt for user configuration automatically
struct SomeControl: ControlWidget { var body: some ControlWidgetConfiguration { AppIntentControlConfiguration( // ... ) .promptsForUserConfiguration() } }
-
15:42 - Custom action hint -> hint treated as verb phrase
struct TimerToggle: ControlWidget { var body: some ControlWidgetConfiguration { AppIntentControlConfiguration( kind: "com.apple.Productivity.TimerToggle", provider: ConfigurableTimerValueProvider() ) { timerState in ControlWidgetToggle( timerState.timer.name, isOn: timerState.isRunning, action: ToggleTimerIntent(timer: timerState.timer) ) { isOn in Label(isOn ? "Running" : "Stopped", systemImage: isOn ? "hourglass" : "hourglass.bottomhalf.filled") .controlWidgetActionHint(isOn ? "Start" : "Stop") } .tint(.purple) } } }
-
16:56 - Specify a display name and add a description
struct TimerToggle: ControlWidget { var body: some ControlWidgetConfiguration { AppIntentControlConfiguration( kind: "com.apple.Productivity.TimerToggle", provider: ConfigurableTimerValueProvider() ) { timerState in ControlWidgetToggle( timerState.timer.name, isOn: timerState.isRunning, action: ToggleTimerIntent(timer: timerState.timer) ) { isOn in Label(isOn ? "Running" : "Stopped", systemImage: isOn ? "hourglass" : "hourglass.bottomhalf.filled") .controlWidgetActionHint(isOn ? "Start" : "Stop") } .tint(.purple) } .displayName("Productivity Timer") .description("Start and stop a productivity timer.") } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。