WidgetKitの新機能、Controlを最短で実装する
概要
iOS 18でWidgetKitにControl
が新機能として追加されました。
このControl
に関しては従来のWidget同様、Widget Extensionで追加するだけでデフォルトでコードが生成され、コントロールセンターやロック画面などのシステムスペースに表示させることができます。ただ初期状態ではControl
をタップしても状態が維持されず、何も機能しない状態です。
そこでこの記事では上記まで追加行程と、その後どのように実装すれば既存のアプリに組み込めるか、ヒントになるよう、まずは軽量な実装として状態を維持して、Control
に反映させるといった実装を最短で試す方法を紹介したいと思います。
最終的にはボタンをタップすると状態を切り替えられる以下のようなものが実装できるようになります。
環境
iOS 18.0 〜
Controlを追加する
まずWidget ExtensionでWidgetを追加します。
追加する際に「Include Control」にチェックを入れます。
とりあえずこれだけでシステムスペース(コントロールセンター、ロック画面)にコントロールが表示されるよになります。
コントロールセンターからコントロールを追加できる様子
追加するとコントロールセンターに表示される
ここまでがアプリにControl
を追加する手順です。
タップ状態を保持してControlに反映させる
続いては実装していきます。
基本的な構成
上記の手順によりいくつかのファイルが生成されますが、その中でXXControlというファイルを開くとControlWidget
に準拠したstructが生成さているのでそのファイルを見ていきます。
struct HogeControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "Hoge",
provider: Provider()
) { value in
ControlWidgetToggle(
"Hoge Timer",
isOn: value,
action: StartTimerIntent()
) { isRunning in
Label(isRunning ? "On" : "Off", systemImage: "timer")
}
}
.displayName("Hoge")
.description("A an example control that runs a timer.")
}
}
上から順に簡単に説明していきます。
-
kind:基本的にはWidgetと同じく
Control
を識別するための文字列です。 -
provider:省略可能な引数で、
ControlValueProvider
もしくはAppIntentControlValueProvider
に準拠したProvider
というstructを渡します。 Providerを渡すことでControl
が更新された時点でcurrentValueの値が渡されす。その値はvalueとしてクロージャーで渡されます。 -
ControlWidgetToggle:
Control
の表示にあたるContentです。ControlWidgetではControlWidgetToggle
とControlWidgetButton
の2種類があり、どちらかを定義する必要があります。今回はControlWidgetToggle
をベースに説明します。- 第一引数:
Control
で表示される文字列です。コントロールセンターに配置時に2×2で表示した場合に表示される文字列です。 - 第二引数:トグルの状態です。初期コードでは
Provider
から受け取ったものを反映しています。 - 第三引数:
Control
をタップした時に発火するアクションです。
- 第一引数:
-
ControlWidgetToggleのクロージャにあるLabel:第一引数で表示される文字列の上に表示されるシンボルアイコンと第一引数で表示される文字列の下に表示される文字列です。シンボルアイコンは1×1の表示時にやロック画面にも表示されます。
-
displayName:
Control
を追加する際に表示される文字列です。 -
description:
Control
の構成中に表示される文字列です。
実装
初期のコードではcurrentValueがtrueになるのでControl
をタップしてもすぐにtrueに戻る状態となっています。
ここをローカルで保持して、状態を変更できるように修正してきます。
まず状態を管理、変更できる当なクラスを作ります。
final class TimerManager {
static let shared = TimerManager()
private init() {}
private(set) var isRunning = false
func setRunning(_ running: Bool) {
self.isRunning = running
}
}
続いてStartTimerIntentのperformで状態を変更する処理を追加します。
func perform() async throws -> some IntentResult {
TimerManager.shared.setRunning(value)
return .result()
}
currentValueで現在の状態を受け取るように修正します。
func currentValue() async throws -> Bool {
TimerManager.shared.isRunning
}
これでControl
をタップする度に状態が変更、保持されます。もう少し見やすく可視化したい方は適当にLabelを修正してください。
Label(isOn ? "On" : "Off", systemImage: isOn ? "gauge.with.needle.fill" : "gauge.with.needle") // コントロールを表示する際のシンボル画像や、タイトル下に表示する文字を定義
.controlWidgetStatus(isOn ? "ON" : "OFF") // 操作後に一瞬表示されるステータスメッセージ
}
.tint(.green) //Imageの色を変更できる
以上です。
参考
Discussion