【Swift UI】Widget(ウィジェット)の実装方法!TimelineProviderとは?
この記事からわかること
- Swift UIでWidgetを実装する方法
- TimelineProviderやTimelineEntry、Timeline構造体の役割と使い方
- placeholder/getSnapshot/getTimelineメソッドの意味
- StaticConfigurationとIntentConfigurationの違い
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
Widget(ウィジェット)とは?
Widget(ウィジェット)とはiOS14以降から追加されたデバイスのホーム画面上にビューを設置できる機能のことです。
アプリ内のデータをホーム画面上に表示できることでアプリを起動させなくてもデータを閲覧できたり、またそこから簡単にアプリへアクセスすることができるようになります。
Widget(ウィジェット)機能はWidgetKit
フレームワークとして提供されておりimport
して使用します。ですがSwiftUIフレームワークを使用しているプロジェクトにのみ導入できるようになっており、UIKitフレームワークの場合は導入できないようです。
実装するにはプロジェクト内に新しくWidget Extensionを追加する必要があります
プロジェクトに「Widget Extension」の追加
アプリ内にWidgetを実装するにはまずメニューから「File」>「 New 」>「Target...」を選択後「Widget Extension」を追加します。
Widget名を記述したら「Inclued Configuration Intent」のチェックを外して「Finish」をクリックします。
するとプロジェクト内に新しくフォルダが生成され中には最初からデモウィジェットが表示できるように記述されたSwiftファイルが作られています。Widgetのみをビルドするには上部のビルド対象を追加したWidget Extensionに変更して「 」ボタンを押します。
StaticConfigurationとIntentConfiguration
作成できるWidgetには2つのプロトコルに準拠したものがあり、作成目的の用途にあった方のプロトコルに準拠した方を選択する必要があります。大きな違いはユーザーがカスタマイズできるプロパティを保持しているかどうかです。
ユーザーが構成可能なプロパティを持たないウィジェット用。株式市場ウィジェットやニュースウィジェットなど
ユーザー設定可能なプロパティを持つウィジェット用。SiriKitカスタムインテントを使用してプロパティを定義。都市の郵便番号が必要な天気予報ウィジェットや追跡番号が必要な荷物追跡ウィジェットなど
この設定の切り替えは「Widget Extension」の追加時に「Inclued Configuration Intent」のチェックを入れるか入れないかによって選択することができます。
コードの中身と役割
Widget Extensionを追加後に生成されるStaticConfigurationの場合のファイルのコードの中身の役割を見ておきます。新規で追加するとサンプルとして現在時刻を表示するビューを持ったWidgetが生成されます。
生成されているのは以下の5つの構造体です。
- Provider:TimelineProviderに準拠
- SimpleEntry:TimelineEntryに準拠
- TestWidgetEntryView:Viewに準拠
- TestWidget:Widgetに準拠
- TestWidget_Previews:PreviewProviderに準拠
それぞれの役割
- Provider:TimelineEntry(表示データと表示時刻)の作成
- SimpleEntry:TimelineEntryの構造を定義
- TestWidgetEntryView:アクティブのTimelineEntryを反映させるビューを構築
- TestWidget:Widgetの説明を定義
- TestWidget_Previews:プレビュー用
import WidgetKit
import SwiftUI
// MARK: - (1)
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date())
}
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)
}
}
// MARK: - (2)
struct SimpleEntry: TimelineEntry {
let date: Date
}
// MARK: - (3)
struct TestWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .time)
}
}
// MARK: - (4)
@main
struct TestWidget: Widget {
let kind: String = "TestWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
TestWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
// MARK: - (5)
struct TestWidget_Previews: PreviewProvider {
static var previews: some View {
TestWidgetEntryView(entry: SimpleEntry(date: Date()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
TimelineProviderプロトコル
// MARK: - (1)
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
}
}
公式リファレンス:TimelineProviderプロトコル
TimelineProvider
プロトコルはウィジェットの表示を更新するタイミングを通知する処理を提供します。メソッドに初期表示されるビューや時間の経過と共に表示させるデータを変更させる処理が備わっています。元から実装されているメソッドは以下の通りです。
- placeholder:仮で表示させたいビュー
- getSnapshot:一時的な(最初の)ビュー&Widget Galleryのプレビュー
- getTimeline:時間と共に変化させる間隔とビュー
これらのメソッドは後述するTimelineEntry
型が大きく関係してきます。
placeholderメソッド
placeholder
メソッドはウィジェットが初めて表示される時のビューをレンダリングします。placeholderとは「仮の確保場所」のような意味を持つのですぐに置き換えられる可能性があり、仮で表示させたいビューを指定します。
サンプルで実装されているウィジェットでは現在時刻がセットされています。
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date())
}
getSnapshotメソッド
getSnapshot
メソッドはWidget Galleryで表示させるプレビューを定義するメソッドです。またWidgetが追加された時に最初に表示されるビューにもなります。Snapshotとは「その瞬間のもの〜」といった意味を持つので一時的な(最初だけの)ビューを指定します。
ここもサンプルでは現在時刻がセットされています。
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
completion(entry)
}
Widget Galleryの表示方法は下記記事を参考にしてください。
getTimelineメソッド
getTimeline
メソッドは表示されているWidgetのビューを更新するタイミング(日付)と更新するデータを提供するメソッドです。後述するTimelineEntry型の構造で日付やデータは保持されます。
このサンプルでは現在時刻と1,2,3,4時間後のTimelineEntryを生成し、entries
プロパティに格納しています。最後はTimeline構造体としてTimelineEntry
配列を返しています。
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 { // 0 〜 4 まで繰り返す
// 現在時刻 , 現在時刻+1時間 , 現在時刻+2時間 ... を生成
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
// TimelineEntry型に日付情報を保持させる
let entry = SimpleEntry(date: entryDate)
// 配列に追加
entries.append(entry)
}
// Widgetを更新するタイムラインを構築
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
Widgetの更新頻度は公式によると1日単位で40〜70回程度、時間間隔にして最短15分おきが推奨されています。
公式リファレンス:Keeping a Widget Up To Date
Timeline構造体
Widgetの更新タイミングはTimeline
構造体によって指定されます。引数にはタイムラインを構築する日付情報を持ったTimelineEntry
型の配列と更新ポリシーを保持しています。
struct Timeline<EntryType> where EntryType : TimelineEntry {
let entries: [EntryType] // タイムラインを構築する日付情報を持ったTimelineEntry型の配列
let policy: TimelineReloadPolicy // 更新ポリシー
}
更新ポリシー(getTimelineメソッドを再度呼び出すタイミング)はTimelineReloadPolicy
の値を指定します。
- .atEnd:配列内の日付が最後までいったら呼び出す
- .after(_ date: Date):指定時刻に呼び出す
- .never:呼び出さない
TimelineEntryプロトコル
TimelineProvider
のメソッドでも扱っていたTimelineEntry
型はWidgetを表示する日付(時刻)とデータを提供するプロトコルです。日付(Date)型のdate
プロパティを保持しており、TimelineEntryが持つ日付(時刻)にWidgetが表示されるような仕組みになっています。
なのでplaceholderとgetSnapshotメソッドには現在時刻をプロパティにセットしていたので即座に表示され、getTimelineでは任意の時間間隔を持たせた日付(時刻)をセットしていたのでその時刻になったら表示されるようになっています。
サンプルではTimelineEntry
プロトコルに準拠させたSimpleEntry
構造体が定義されています。
// MARK: - (2)
struct SimpleEntry: TimelineEntry {
let date: Date
}
実際のビュー:View
Widgetに表示させるビュー構造を記述する構造体です。この中に表示させたいビューを好きなように配置していきます。ここにHello World!
のような固定値を入力することもできますが、主にアクティブになっているentry
に定義されているデータを参照することで可変的なビューを構築していくことが多いと思います。
// MARK: - (3)
struct TestWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
HStack{
Image(systemName: "iphone").font(.system(size: 40))
VStack{
Text("Hello World!")
Text(entry.date, style: .time)
}
}
}
}
Widgetの説明:Widget
表示されるWidget名とWidgetの説明を定義する構造体です。この構造体の中に最初に紹介したStaticConfiguration
またはIntentConfiguration
が定義されそのメソッドとして名称や説明を定義することができます。チェックの有無で自動的に適したConfiguration構造体が記述されます。
- kind:Widgetを識別する一意の文字列
- provider:ビューの更新のタイミングを管理するオブジェクト
- configurationDisplayName:Widget名
- description:Widgetの説明
// MARK: - (4)
@main
struct TestWidget: Widget {
let kind: String = "TestWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
TestWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
プレビュー:PreviewProvider
// MARK: - (5)
struct TestWidget_Previews: PreviewProvider {
static var previews: some View {
TestWidgetEntryView(entry: SimpleEntry(date: Date()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
Widgetにも通常のSwift UIで作成したビューのようにプレビューを表示させるコードが記述されています。表示させるサイズは列挙型WidgetFamily
の値から指定して変更できます。iOSでは3種類(小・中・大)、iPadでは4種類(小・中・大・特大)から選択できます。
enum WidgetFamily {
case systemSmall // 小サイズ
case systemMedium // 中サイズ
case systemLarge // 大サイズ
case systemExtraLarge // 特大サイズ
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。