WidgetKitの導入で開発に必要なことをまとめました。
Widget(ウィジェット)とは?
Widget(ウィジェット)はiOS14から追加された機能で、アプリを開くことなくホーム画面上でアプリの一部機能(最新情報の表示など)を利用することが可能となります。
1. サイズ
ウィジェットには「小、中、大」のサイズがあります。(iPadOS15でXLサイズも登場しました)
※全てのサイズに対応する必要はありません。
2. 種類
一つのアプリに複数種類のウィジェットを追加することも可能です。
3. プレースホルダー
プレースホルダーはデータを読み込み中(サーバーから取得中等)の時に表示されるものです。
パーツを配置すると自動的にプレースホルダー時の表示も設定されますが、プレースホルダーの表示をカスタムすることも可能です。(一部のパーツをプレースホルダー化せずに固定表示したりなど)
.unredacted()
をつけるとプレースホルダー時にも表示されます。
struct WidgetSampleEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
HStack {
Image(decorative: "WidgetKitIcon")
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
.unredacted() // プレースホルダー化しない設定
VStack {
Text("sample")
Text("message")
}
}
Text(entry.date, style: .time)
}
}
}
表示データ
IntentTimelineProviderのfunc placeholder(in context: Context) -> SimpleEntry
でプレースホルダー用のエントリを返します。
表示期間
ウィジェットが更新されるタイミングでfunc getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ())
が呼ばれて、completionを実行するまでの間にプレースホルダーが表示されることになります。
4. ウィジェットギャラリー
ウィジェットギャラリーは表示するウィジェットを選択する箇所になります。
以下の項目が設定可能です。
- ウィジェット名
- 簡単な説明
- 追加ボタンの色
.configurationDisplayName("My Widget")
がウィジェット名
.description("This is an example widget.")
が簡単な説明
5. ディープリンク
ディープリンクでウィジェットからアプリを起動したときに任意の画面を表示させたりすることができます。
小サイズのウィジェットだけリンクの設定数が1つだけとなっています。
中、大サイズのウィジェットはViewごとにリンクの設定ができます。
リンクの設定方法は2つあります。
// Linkで囲むパターン
Link(destination: URL(string: "widgetsample://deeplink?test=1")!) {
Text("リンク")
}
// .widget(_ url: URL)を付加するパターン
Text("リンク").widgetURL(URL(string: "widgetsample://deeplink?test=1")!)
別途URLスキームの設定が必要になります。
6. 角丸(ContainerRelativeShape)
ウィジェット本体の角に合わせて自然な角丸の設定を行うためにはContainerRelativeShapeを使用します。
.clipShape(ContainerRelativeShape())
をつけることで角丸の設定ができます。
struct WidgetSampleEntryView : View {
var body: some View {
VStack {
HStack {
Image(decorative: "WidgetKitIcon")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
.clipShape(ContainerRelativeShape()) // 自然な角丸
Spacer()
}
Spacer()
}
.padding(8)
}
}
7. プレビュー
WidgetExtension追加時に自動でPreviewProviderが実装されるので右側にプレビューが表示されます。
.redacted(reason: .placeholder)
をつけるとプレースホルダー表示を確認できます。
struct Widget_Previews: PreviewProvider {
static var previews: some View {
WidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
.redacted(reason: .placeholder)
}
}
上が通常、下が.redacted(reason: .placeholder)
をつけた状態
.environment(\.colorScheme, .dark)
をつけるとダークモード時の表示を確認できます。
8. 更新タイミング
まず初めにウィジェット(タイムライン)の更新タイミングは基本的にOS任せです。(必ずこの時間に更新するということはできません)
OSは以下のような多くの要因によってウィジェットの更新タイミングを自動調整します。
- ウィジェットがユーザーに表示される頻度と間隔
- ウィジェットの最後のリロード時間
- ウィジェットを含むアプリがアクティブかどうか
次にウィジェットの更新の仕組みについて説明します。
ウィジェットは「タイムラインの更新」と「タイムラインにエントリを追加」することで定期的に表示を更新します。
下の図は公式ドキュメントにあるものです。
タイムラインは2時間間隔で更新されています。(この時にサーバーからデータの取得等を行います。)
タイムラインには1時間ごとに更新されるエントリを追加しています。
最後にRefresh: .never
でタイムラインの更新を停止しています。
タイムラインの次回更新予定(TimelineReloadPolicy)の種類
- .atEnd:エントリを全て表示した後にタイムラインを更新
- .never:更新しない
- .after(_ date: Date):指定した日時に更新(途中でタイムラインの更新が走った場合残りのエントリは実行されない)
・.after(_ date: Date)で15分未満の間隔で更新するように設定しても、設定した通りには更新されません。
・15分以上の間隔に設定しても必ずその時間に更新される保証はありません。
・公式のドキュメントの図では綺麗に2時間ごとに更新されているように描かれていますが、実際はばらつきがあり3時間後とかになることもあります。(エントリは正確に更新されます)
9. カウントダウンText
ウィジェットを使う場合、リアルタイムな時間情報を表示したいケースがあります。
例えば「よく使う駅で電車があと何分で出発するか」「予定のスケジュールがあと何分後か」など
ですが8.更新タイミングでも説明した通り、秒単位でウィジェットを更新することは出来ません。
そんな時に使えるのがTextのtimerスタイルです
以下のようにWidget内にTextを配置するだけでカウントダウンするTextを表示することができます。
struct WidgetEntryView : View {
var body: some View {
let components = DateComponents(minute: 5)
let futureDate = Calendar.current.date(byAdding: components, to: Date())!
// 5分間のカウントダウンを行うテキスト
Text(futureDate, style: .timer)
}
}
Yahoo乗換案内では以下のように電車が出発するまでの時間をカウントダウンしてくれています。
10. UserDefaults
アプリ側で呼び出すUserDefaults.standard
と、ウィジェット側で呼び出すUserDefaults.standard
は別のデータになります。
アプリ側とウィジェット側でUserDefaultsを共有するには特別な設定が必要です。
詳細は下のブログで説明しています。
-
WidgetKitでUserDefaultsを使う + タイムラインの更新間隔を計測してみる
アプリ側とウィジェット側でUserDefaultsを共有するには設定が必要です。App Groupsでグルーピングすることで共通のUserDefaultsを利用できるようになります。 今回やりたかった ...
11. メモリ上限
公式のドキュメントに記載はありませんが30MBがウィジェットのメモリ上限になっています。
Appleの開発者フォーラムで記載があります。