はじめに
suzukiさんが以前の記事でWidget機能ついて紹介してくれていますが、今回はxcode12も正式リリースされましたし、より実践的なWidgetKitの使い方を紹介したいと思います。
Today Extensionとの違い
今までiOSのWidgetといえばToday Extensionでしたが、WidgetKitとはどのような違いがあるか以下にまとめました。
- ホーム画面に設置可能
- SwiftUIのみ対応
- ユーザーアクションはシングルタップのみ、スクロールやスイッチなどのUIコンポーネントは設置できない
- サイズは固定の3種類のみ(.systemSmall(2×2サイズ), .systemMedium(4×2サイズ), .systemLarge(4×4サイズ))
また、WWDC20にてWidgetKitの目的は
- Glanceable(ひと目でわかる)
- Relevant(関連性)
- Personalaized (パーソナライズ)
とのことなので、ホーム画面にてユーザーに関連する情報が一目でわかるようにするのがWidgetKitなようなので、Today Extensionではミニアプリ的な側面もありましたが、WidgetKitでは完全に別物としてデザインしなければ行けないと思います。
ターゲットの追加
ターゲットの追加方法に関しましては、以前の記事でご確認ください。
StaticConfiguration
StaticConfigurationとはInclude Configuration Intentチェックなしで追加される、ユーザー設定可能なプロパティを持たないWidgetです。
TimelineProvider
WidgetではTimelineという、いつどのViewを表示するか管理する仕組みを実装します。
まずはEntryを定義します。
1 2 3 | struct SimpleEntry: TimelineEntry { let date: Date } |
続いて、TimelineProviderの実装です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | struct Provider: TimelineProvider { func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date()) completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } |
getSnapshot
SnapshotはWidgetのプレビュー機能やWidget Galleryで即座にWidgetを表示するために使われます。 SnapshotはひとつのTimelineEntryを受け取ります。 Widgetのコンテンツが提供されるまではモック的な値を入れておきます。
getTimeline
getTimelineは、実際のウィジェット表示に使用されます。TimelineはTimelineEntryオブジェクトの配列です。Timelineの更新は最後のTimelineEntryを表示したとき(.atEnd), 指定した時間が経過した後(.after), アプリからWidgetCenterを利用して更新通知されたら更新する(.never)の3種類があります。 TimelineはTimelineProviderに準拠したProviderによってwidgetsに渡されます。
View
Viewには実際に表示するWidgetを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | struct CharacterWidgetEntryView : View { @Environment(\.widgetFamily) var family: WidgetFamily var entry: Provider.Entry @ViewBuilder // Viewにバリエーションがある時に宣言する var body: some View { switch family { case .systemSmall: Text(entry.date, style: .time); Text("small") case .systemMedium: Text(entry.date, style: .time); Text("medium") case .systemLarge: Text(entry.date, style: .time); Text("large") @unknown default: fatalError() } } } |
Widgetのサイズは3種類あるのですが、上記のように現在のサイズが取得できるので、表示をサイズによって変更するようにしています。
IntentConfiguration
IntentConfigurationとはInclude Configuration Intentチェックありで追加される、ユーザー設定可能なプロパティを持つWidgetです。例をあげると、天気のWidgetなど、Widgetを長押しでユーザー設定の変更が可能な機能を持つWidgetになります。
IntentTimelineProvider
今回は、ユーザーが設定できる情報nameを追加したEntryを作成します。
1 2 3 4 5 | struct SimpleEntry: TimelineEntry { let date: Date var name: String } |
TimelineProviderとの違いは各関数で、インテントの選択を表す追加のパラメータConfigurationIntentを取得します。また、「typealias Intent = ConfigurationIntent」を追記しておきます。「ConfigurationIntent」については後述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | struct Provider: IntentTimelineProvider { typealias Intent = ConfigurationIntent func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), name: "") } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), name: "") completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate, name: configuration.Name ?? "") entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } |
Widget設定の変更
ユーザーがWidget設定の変更ができるようにしていきたいと思います。まずはIntentsフレームワークをTargetから追加します。
今回はUI要素は不要なため「Include UI Extension」のチェックは外しておきます。
作成したSampleIntentsのターゲットページに、Supported Intentsという名前のセクションがあります。今回はConfigurationIntentという名前のアイテムを新規で作追加します。
Include Configuration Intentチェックありで作成していれば、.intentdefinitionファイルが作成されているはずなので、先ほど作成したSampleIntentsをターゲットに追加します。
今回はEntryに追加したnameの情報をユーザーに変更させたいのでParametersにNameを追加します。
ここまでやったら、SampleIntentsのIntentHandler.swiftファイルにConfigurationIntentHandlingを追記すると、Xcodeが関数を自動生成を促してくれると思います。
1 | class IntentHandler: INExtension, ConfigurationIntentHandling { |
自動生成されたprovideNameOptionsCollection関数を以下のように編集します。
1 2 3 4 5 6 7 8 9 | func provideNameOptionsCollection(for intent: ConfigurationIntent, searchTerm: String?, with completion: @escaping (INObjectCollection<NSString>?, Error?) -> Void) { let nameIdentifiers: [NSString] = [ "佐藤", "田中", "鈴木" ] let allNameIdentifiers = INObjectCollection(items: nameIdentifiers) completion(allNameIdentifiers, nil) } |
そうしたらViewのbodyにnameを表示するように編集します。
1 2 3 4 5 6 | var body: some View { Text(entry.name) .font(.headline) .padding(1) Text(entry.date, style: .time) } |
これで準備は完了です。Widgetを長押しすると「ウィジェットを編集」が表示されると思います。
アプリからWidgetを更新
アプリが通知を受けて更新するなどといった場合など、アプリ側からWidgetを更新することが可能です。
以下を呼び出すことで、アプリのWidgetすべてをリロードさせることが可能です。
1 | WidgetCenter.shared.reloadAllTimelines() |
また、アプリ内で複数のWidgetを管理している場合、特定のWidgetを更新したい場合は以下のように行います。
1 | WidgetCenter.shared.reloadTimelines(ofKind: "com.example.WidgetSample.WidgetTest1") |
さいごに
今回のWidget機能はできることが絞られている分、デザインの大事さを痛感しますね。ミニアプリのような使い方ではないのでAndroidのWidgetとも違い、リッチなアイコンのような印象です。