WWDC Quick Look 💓 By SwiftGGTeam
WidgetKit の基礎

WidgetKit の基礎

元の動画を見る

ハイライト

WidgetKit は Timeline(タイムライン)という仕組みによって、Widget を独立プロセス内で必要に応じて描画します。開発者はデータエントリと SwiftUI ビューを提供し、システムが適切な時刻に適切な内容を表示します。さらに reload policy によって更新頻度を制御し、鮮度とバッテリー消費のバランスを取ります。

主要内容

なぜ Widget には独立プロセスが必要なのか

あなたの App は SwiftUI で書かれているかもしれませんし、UIKit で書かれているかもしれません。しかし Widget は一律に SwiftUI で構築します。Widget は独立した Widget Extension プロセスで動き、メイン App の一部ではありません。

つまり、メイン App と Widget はメモリを直接共有できません。データの受け渡しは App Group の共有コンテナを通す必要があります。たとえば共有 UserDefaults や共有データベースです。(02:30)

この設計のコストは、データ同期が一段複雑になることです。利点は、システムがメイン App を起動せずにいつでも Widget を描画できることです。Widget のビューはシステムによってアーカイブされキャッシュされるため、時刻が来たらそのまま表示でき、コードを動かし続ける必要がありません。

よい Widget の3つの基準

Apple は memorable widget の基準を3つの言葉にまとめています。(01:03)

  • Glanceable(一目でわかる):ユーザーが一目で情報を得られること。天気 Widget は現在の気温と天気だけを表示し、展開操作を必要としません。
  • Relevant(関連性がある):内容が時間、場所、ユーザーの習慣に応じて変わること。カレンダー Widget は朝には当日の会議を表示し、午後には夜の予定へ自動で切り替わります。
  • Personalizable(個人に合わせられる):ユーザーが何を表示するか設定できること。写真 Widget は特定のアルバムを選び、家族キャンプの写真だけを表示できます。

この3つの基準が、その Widget がユーザーのホーム画面に長く残るかどうかを決めます。

Timeline:Widget の心臓部

WidgetKit は Extension に「現在のビュー」を要求しません。代わりに、将来の複数の時点に対応するビューのデータを要求します。これが Timeline です。(03:12)

各 Timeline は複数の Timeline Entry で構成されます。各 Entry は特定時刻のデータを持ちます。システムはこれらのビューを事前に描画してアーカイブし、時刻が来たら直接表示します。

Timeline Provider は3つの状態を扱う必要があります。(05:26)

  • Snapshot:Widget ギャラリー内のプレビュー画像です。これは第一印象なので、現実的で魅力的なデータを使うべきです。たとえば読書 App なら、人気書籍の表紙と進捗をデフォルトで表示できます。
  • Placeholder:Widget の初回読み込み時に表示されるプレースホルダービューです。同期的に返す必要があり、ディスク読み込みやネットワークアクセスはできません。SwiftUI の .redacted() modifier を使うと簡略表示を素早く作れます。
  • Timeline:実際のタイムラインエントリの列です。各エントリには、その時点を描画するために必要なすべてのデータが含まれます。

更新戦略:3種類の reload policy

Timeline の更新動作は reload policy で制御します。3つの選択肢はそれぞれ異なる場面に対応します。(07:55)

  • atEnd:Timeline のエントリが尽きたら自動更新します。1日の読書リマインダーのように、内容の列が固定されておらず継続的に補充が必要な場面に向きます。
  • afterDate:指定した時刻に更新します。毎日午前0時に読書予定を再計算する Widget のように、明確な更新時刻がある場面に向きます。
  • never:自動更新しません。読書進捗 Widget のように、ユーザー操作や App 状態の変化があった時だけ更新すればよい場面に向きます。必要な時は WidgetCenter の reload API やプッシュ通知で明示的に更新します。

WidgetKit は各 Widget に更新予算を割り当てます。予算の大きさはユーザーがその Widget をどれくらい見るかに影響されます。前面で頻繁に更新を呼ぶとスロットリングされる可能性があります。App がバックグラウンドへ入る直前の最後の reload 呼び出しが、たいてい最適なタイミングです。(10:27)

スポーツの試合のように、内容に明確な開始・終了時刻があり、高頻度更新とプッシュ通知が必要なら、Widget ではなく Live Activity を検討してください。

Widget と App をつなぐ3つの方法

Widget は孤立した存在ではありません。3つの仕組みがメイン App と結び付けます。(13:25)

  • Deep Link:Widget をタップすると App の特定画面へ直接移動します。読書 Widget なら App のホームではなく、現在読んでいる本の詳細ページへジャンプできます。
  • 設定可能な Widget:ユーザーが Widget に表示する内容を選べます。天気 Widget は場所を選べ、読書 Widget は追跡する本を選べます。
  • インタラクティブ要素:ボタンやスイッチにより、ユーザーは Widget 上で直接操作できます。章を読み終えた後、App を開かずに Widget 上のボタンでチェックインできます。

インタラクティブ要素は App Intent で実装します。Widget のビューはシステムがアーカイブして描画するため、実行時にあなたのコードが動いているわけではありません。そのためボタンやスイッチは App Intent にバインドされ、ユーザー操作時にシステムが代わりに実行します。(16:45)

Tinted と Clear モードへの対応

iOS は Tinted(着色)と Clear(透明)という2種類のシステムカスタマイズモードをサポートします。これらのモードでは、システムが Widget 背景をガラス素材へ置き換え、コンテンツに色調処理を加えます。(17:23)

多くの場合 SwiftUI がうまく処理しますが、画像では問題が起きることがあります。たとえば本の表紙が Clear モードで白い四角になる場合があります。システムがその画像を正しく着色できないためです。

解決策は .widgetAccentedRenderingMode(.fullColor) modifier です。これにより、その領域を元の色のまま描画するようシステムへ伝えます。(18:17)

詳細

最初の Widget を作る

(03:50)

Xcode で Widget Extension を作成すると、基本コードが自動生成されます。最も単純な Widget には、Widget 構造体、Timeline Provider、SwiftUI ビューの3つが必要です。

struct DailyReadingGoalWidget: Widget {
    let kind = "DailyReadingGoalWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: kind,
            provider: DailyReadingGoalProvider()
        ) { entry in
            DailyReadingGoalView(book: entry.book,
                                 message: entry.message,
                                 timeOfDay: entry.timeOfDay)
            .environment(\.colorScheme, .dark)
            .containerBackground(for: .widget) {
                Background()
            }
        }
    }
}

重要な点:

  • kind は Widget の一意な識別子です。システムはこれで Widget の種類を区別します。
  • StaticConfiguration は設定できない Widget に使います。ユーザーがパラメータを設定する必要がある場合は AppIntentConfiguration を使います。
  • provider は Timeline Entry を生成し、データ層の中心になります。
  • クロージャ引数 entry は単一時点のデータです。返された SwiftUI ビューはシステムによってアーカイブされます。
  • .containerBackground(for: .widget) は Widget の背景ビューを宣言します。これは Tinted/Clear モード対応の鍵です。システムはどの View が背景かを知って初めて、それをガラス素材へ置き換えられます。
  • .environment(\.colorScheme, .dark) はダークモードを強制し、明るいホーム画面上での可読性を保ちます。

Widget が対応するサイズを制限する

(12:25)

Widget は複数のサイズ(family)をサポートします。systemSmall、systemMedium、systemLarge、そして iOS 27 で追加された systemExtraLargePortrait などです。できるだけ多くのサイズをサポートするのが望ましいですが、すべての Widget がすべてのサイズに向くわけではありません。

struct DailyReadingGoalWidget: Widget {
    let kind = "DailyReadingGoalWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: kind,
            provider: DailyReadingGoalProvider()
        ) { entry in
            DailyReadingGoalView(book: entry.book,
                                 message: entry.message,
                                 timeOfDay: entry.timeOfDay)
            .environment(\.colorScheme, .dark)
            .containerBackground(for: .widget) {
                Background()
            }
        }
        .supportedFamilies([.systemMedium])
    }
}

重要な点:

  • .supportedFamilies は利用可能な Widget サイズを制限します。ここでは systemMedium のみ対応しています。
  • 同じ Widget は同じ Timeline Provider を再利用しつつ、サイズごとに異なる SwiftUI ビューを提供できます。
  • systemExtraLargePortrait は visionOS 26 で初めて導入され、iOS 27、macOS 27、iPadOS 27 でも対応しました。この縦長の超大型サイズは、情報フィードや予定表の表示に向いています。

(14:03)

デフォルトでは、Widget をタップすると App のホーム画面が開きます。Widget が特定の内容を表示しているなら、その内容へ直接移動する Deep Link を用意すべきです。

struct DailyReadingGoalWidget: Widget {
    let kind = "DailyReadingGoalWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: kind,
            provider: DailyReadingGoalProvider()
        ) { entry in
            DailyReadingGoalView(book: entry.book,
                                 message: entry.message,
                                 timeOfDay: entry.timeOfDay)
            .environment(\.colorScheme, .dark)
            .containerBackground(for: .widget) {
                Background()
            }
            .widgetURL(URL(string: "bookclub://reading/\(book.bookID)"))
        }
        .supportedFamilies([.systemMedium])
    }
}

重要な点:

  • .widgetURL は Widget ビューに URL を付加します。Widget がタップされると、システムはこの URL を App に渡します。
  • URL には書籍 ID が含まれています。App 起動後にこの URL を解析し、対応する書籍の詳細ページへ直接遷移します。
  • Deep Link は Widget と App の間の文脈を保ちます。ユーザーが Widget から App へ移動しても迷子になりません。

Tinted モードでの画像描画を制御する

(18:17)

Tinted モードでは、システムはホーム画面の視覚的統一感を保つため、Widget の内容を単色調へ変換しようとします。しかし写真、表紙、ロゴのように元の色を保つ必要がある内容は壊れることがあります。

struct BookCoverImage: View {
    let imageName: String

    var body: some View {
        Image(imageName: bundle: .main)
            .widgetAccentedRenderingMode(.fullColor)
    }
}

重要な点:

  • .widgetAccentedRenderingMode(.fullColor) は、この画像をフルカラーで描画し、色調処理に参加させないようシステムへ伝えます。
  • もう1つの選択肢は .accented で、画像をシステムの色調処理に参加させます。純粋なアイコン系の内容に向いています。
  • .fullColor を乱用すると Tinted モードの視覚的統一感が壊れます。ユーザーアイコン、書籍表紙、ブランドロゴなど、元の色を保つ必要がある核心的な視覚要素だけに使います。
  • 背景やその他の補助要素はシステム Tint に追従すべきです。全体に .fullColor を付けないでください。

重要な示唆

  • 読書トラッキング Widget を作るStaticConfigurationafterDate の更新戦略を使い、毎日午前0時に読書計画を再計算します。systemMedium では今日の目標を表示し、systemExtraLargePortrait では今後3日間の読書予定を表示します。Deep Link で現在の章へ直接移動します。入口 API:StaticConfigurationTimelineProviderwidgetURL

  • 設定可能な複数都市の天気 Widget を作るAppIntentConfiguration を使って、ユーザーが注目する都市を選べるようにします。同じ Widget を複数配置し、それぞれ異なる設定にできます。1つのホーム画面に3つの Widget を置き、自宅、会社、旅行先の天気をそれぞれ追跡できます。入口 API:AppIntentConfigurationAppIntent

  • インタラクティブな習慣チェックイン Widget を作る:Widget 上にボタンを置き、タップすると今日の習慣を直接完了します。ボタンは App Intent にバインドしてデータを更新し、WidgetCenter.reloadTimelines で Widget 状態を更新します。入口 API:ButtonAppIntentWidgetCenter

  • 既存 Widget を Tinted/Clear モードへ対応させる:すべての Widget 画像を確認し、元の色を保つ必要がある内容には .widgetAccentedRenderingMode(.fullColor) を追加します。すべての Widget ルートビューには .containerBackground(for: .widget) を追加します。Xcode SwiftUI Preview で rendering mode を切り替え、素早く検証します。入口 API:widgetAccentedRenderingModecontainerBackground

  • Widget をクロスプラットフォームで再利用する:iOS Widget は CarPlay と macOS(remote widget として)で自動的に利用できます。テストでは、Deep Link とインタラクションが macOS でも正常に動くか確認し、マウスクリックとタッチ操作の間で操作感を揃えます。入口 API:supportedFamilieswidgetURL

関連 Session

コメント

GitHub Issues · utterances