【Swift UI】Widget(ウィジェット)の実装方法!TimelineProviderとは?

この記事からわかること

  • Swift UIWidget実装する方法
  • TimelineProviderTimelineEntryTimeline構造体役割使い方
  • placeholder/getSnapshot/getTimelineメソッドの意味
  • StaticConfigurationIntentConfiguration違い

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

参考文献
Meet WidgetKit
Creating a Widget Extension
Specifications

Widget(ウィジェット)とは?

Widget(ウィジェット)とはiOS14以降から追加されたデバイスのホーム画面上にビューを設置できる機能のことです。

【Swift UI】Widget(ウィジェット)の実装方法!

アプリ内のデータをホーム画面上に表示できることでアプリを起動させなくてもデータを閲覧できたり、またそこから簡単にアプリへアクセスすることができるようになります。

Widget(ウィジェット)機能はWidgetKitフレームワークとして提供されておりimportして使用します。ですがSwiftUIフレームワークを使用しているプロジェクトにのみ導入できるようになっており、UIKitフレームワークの場合は導入できないようです。

実装するにはプロジェクト内に新しくWidget Extensionを追加する必要があります

プロジェクトに「Widget Extension」の追加

アプリ内にWidgetを実装するにはまずメニューから「File」>「 New 」>「Target...」を選択後「Widget Extension」を追加します。

【Swift UI】Widget(ウィジェット)の実装方法!

Widget名を記述したら「Inclued Configuration Intent」のチェックを外して「Finish」をクリックします。

【Swift UI】Widget(ウィジェット)の実装方法!

するとプロジェクト内に新しくフォルダが生成され中には最初からデモウィジェットが表示できるように記述されたSwiftファイルが作られています。Widgetのみをビルドするには上部のビルド対象を追加したWidget Extensionに変更して「 」ボタンを押します。

【Swift UI】Widget(ウィジェット)の実装方法!

StaticConfigurationとIntentConfiguration

作成できるWidgetには2つのプロトコルに準拠したものがあり、作成目的の用途にあった方のプロトコルに準拠した方を選択する必要があります。大きな違いはユーザーがカスタマイズできるプロパティを保持しているかどうかです。

StaticConfiguration

ユーザーが構成可能なプロパティを持たないウィジェット用。株式市場ウィジェットやニュースウィジェットなど

IntentConfiguration

ユーザー設定可能なプロパティを持つウィジェット用。SiriKitカスタムインテントを使用してプロパティを定義。都市の郵便番号が必要な天気予報ウィジェットや追跡番号が必要な荷物追跡ウィジェットなど

この設定の切り替えは「Widget Extension」の追加時に「Inclued Configuration Intent」のチェックを入れるか入れないかによって選択することができます。

【Swift UI】Widget(ウィジェット)の実装方法!

コードの中身と役割

Widget Extensionを追加後に生成されるStaticConfigurationの場合のファイルのコードの中身の役割を見ておきます。新規で追加するとサンプルとして現在時刻を表示するビューを持ったWidgetが生成されます。

生成されているのは以下の5つの構造体です。

  1. Provider:TimelineProviderに準拠
  2. SimpleEntry:TimelineEntryに準拠
  3. TestWidgetEntryView:Viewに準拠
  4. TestWidget:Widgetに準拠
  5. TestWidget_Previews:PreviewProviderに準拠

それぞれの役割

  1. Provider:TimelineEntry(表示データと表示時刻)の作成
  2. SimpleEntry:TimelineEntryの構造を定義
  3. TestWidgetEntryView:アクティブのTimelineEntryを反映させるビューを構築
  4. TestWidget:Widgetの説明を定義
  5. 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プロトコルはウィジェットの表示を更新するタイミングを通知する処理を提供します。メソッドに初期表示されるビューや時間の経過と共に表示させるデータを変更させる処理が備わっています。元から実装されているメソッドは以下の通りです。

これらのメソッドは後述する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の表示方法は下記記事を参考にしてください。

【iPhone】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構造体

公式リファレンス:Timeline構造体

Widgetの更新タイミングはTimeline構造体によって指定されます。引数にはタイムラインを構築する日付情報を持ったTimelineEntry型の配列更新ポリシーを保持しています。

struct Timeline<EntryType> where EntryType : TimelineEntry { 
  let entries: [EntryType] // タイムラインを構築する日付情報を持ったTimelineEntry型の配列
  let policy: TimelineReloadPolicy // 更新ポリシー
}

更新ポリシー(getTimelineメソッドを再度呼び出すタイミング)はTimelineReloadPolicyの値を指定します。

TimelineEntryプロトコル

公式リファレンス: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)
            }
        }
    }
}
【Swift UI】Widget(ウィジェット)の実装方法!

Widgetの説明:Widget

表示されるWidget名とWidgetの説明を定義する構造体です。この構造体の中に最初に紹介したStaticConfigurationまたはIntentConfigurationが定義されそのメソッドとして名称や説明を定義することができます。チェックの有無で自動的に適したConfiguration構造体が記述されます。

// 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種類(小・中・大・特大)から選択できます。

公式リファレンス:WidgetFamily

enum WidgetFamily {
  case systemSmall      //  小サイズ
  case systemMedium     //  中サイズ
  case systemLarge      //  大サイズ
  case systemExtraLarge //  特大サイズ
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index