WWDC Quick Look 💓 By SwiftGGTeam
What’s new in widgets

What’s new in widgets

元の動画を見る

Highlight

WWDC25 における Widget のアップデートは大きく 3 つです。visionOS と CarPlay へのプラットフォーム拡張、watchOS に向けた「関連する時間帯にだけ表示される」Relevance Widget の導入、そして APNs プッシュによる Widget の直接更新です。


核心となる内容

ここ数年、Widget には 2 つの課題が残されていました。1 つはプラットフォームのカバレッジ不足で、visionOS にネイティブ Widget が存在せず、CarPlay でも手出しができない状態でした。もう 1 つはリフレッシュの仕組みが Timeline のみで、開発者は数時間先のデータを事前に予測しておくか、システムのスロットルに沿った順番待ちを受け入れるしかなく、サーバー側に新しいデータが届いても能動的に Widget へ通知する手段がありませんでした。

今回の WWDC25 では、この 2 つを同時に解決しています。Widget・Live Activity・Control が visionOS 26、CarPlay、macOS Tahoe のメニューバーに揃って登場しました。visionOS 上の Widget は壁に貼り付けたり木製のデスクに埋め込んだりでき、遠くから見ると簡略化されたレイアウトに自動で切り替わります。CarPlay では StandBy 風のスタイルで Widget をダッシュボードの左側に配置できます。watchOS 26 には Relevance Widget が追加され、たとえば「happy hour の 17:00–19:00 にだけ関連がある」と宣言すれば、その時間帯を過ぎると Smart Stack から自動的に消える、といった挙動が可能になりました。そしてもっとも重要なのが WidgetKit Push Notifications です。サーバーから content-changed: true のサイレントプッシュを送るだけで Widget がただちに更新され、Timeline スケジューラの時間割に縛られなくなります。


詳細

Accented Rendering Mode

iOS 26 のホーム画面には、clear glass と単色 tint という 2 つの新しいテーマが追加されました。システムはまず Widget の内容を accented rendering mode でレンダリングし(すべての要素を白に統一)、そのあと背景を取り除いてガラス調や単色に置き換えます。一般的な Widget であれば修正は不要ですが、写真やブランドカラーを使った Widget は問題が起きます。画像が一面真っ白になってしまい、まともに見えなくなるためです。

Apple が示す解決策は、widgetRenderingMode という environment 値を観察し、現在のモードに応じてレイアウトを分岐させる方法です(02:44)。

struct MostFrequentBeverageWidgetView: View {
    @Environment(\.widgetRenderingMode) var renderingMode
    
    var entry: Entry
    
    var body: some View {
        ZStack {
            if renderingMode == .fullColor {
                Image(entry.beverageImage)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            
                LinearGradient(gradient: Gradient(colors: [.clear, .clear, .black.opacity(0.8)]), startPoint: .top, endPoint: .bottom)
            }
            
            VStack {
                if renderingMode == .accented {
                    Image(entry.beverageImage)
                        .resizable()
                        .widgetAccentedRenderingMode(.desaturated)
                        .aspectRatio(contentMode: .fill)
                }
                
                BeverageTextView()
            }
        }
    }
}

ポイント:

  • @Environment(\.widgetRenderingMode): システムが現在どのレンダリングモードを使っているかを取得します。値は .fullColor または .accented です。
  • if renderingMode == .fullColor: full-color モードでは大きな画像とグラデーションのオーバーレイを表示します。
  • if renderingMode == .accented: accented モードではレイアウトを切り替え、画像をテキストの上に配置します。
  • .widgetAccentedRenderingMode(.desaturated): accented モードで画像を一面白に塗りつぶす代わりに、グレースケールのディテールを残します。この modifier は .accented .accentedDesaturated .fullColor nil の 4 種類の値を取ります。アルバムジャケットや書籍の表紙では .fullColor が推奨です。

visionOS Widget の構成

visionOS 26 では Widget を壁に固定したり、デスクに埋め込んだりできます(06:08)。

struct CaffeineTrackerWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: "BaristaWidget",
            provider: Provider()
        ) { entry in
            CaffeineTrackerWidgetView(entry: entry)
        }
        .configurationDisplayName("Caffeine Tracker")
        .description("A widget tracking your caffeine intake during the day.")
        .supportedMountingStyles([.elevated])
        .widgetTexture(.paper)
        .supportedFamilies([.systemExtraLargePortrait])
    }
}

ポイント:

  • .supportedMountingStyles([.elevated]): 「壁から浮き出すように設置する」スタイルだけをサポートすると宣言します。もう 1 つの値は .recessed(木製デスクに埋め込む)です。この modifier は iOS でも利用できます。
  • .widgetTexture(.paper): visionOS 専用です。デフォルトのガラス質感をポスター紙のような質感に置き換えます。
  • .supportedFamilies([.systemExtraLargePortrait]): visionOS 26 で追加された縦向きの超大型サイズです。従来の systemExtraLarge は横向き版です。

Level of Detail: 距離に応じて自動で簡略化

visionOS では Widget を部屋の反対側の壁に貼ることもでき、遠くから見ると文字が小さくなり、ボタンも押しづらくなります。新たに追加された levelOfDetail environment 値はこの問題を解決します(08:56)。

struct CaffeineTrackerWidgetView : View {
    @Environment(\.levelOfDetail) var levelOfDetail
    
    var entry: CaffeineLogEntry

    var body: some View {
        VStack(alignment: .leading) {
            TotalCaffeineView(entry: entry)

            if let log = entry.log {
                LastDrinkView(log: log)
            }

            if levelOfDetail == .default {
                LogDrinkView()
            }
        }
    }
}

ポイント:

  • levelOfDetail の値は .default または .simplified で、距離に応じてシステムが自動的に切り替えます。
  • 遠距離では LogDrinkView を非表示にします。離れた場所からはボタンが押しづらいので、まず取り除きます。
  • 遠距離では TotalCaffeineView のフォントサイズを大きくし(次のコード参照)、「カフェインの合計量」というコア情報をより読みやすくします。
  • level of detail の切り替えにはトランジションアニメーションが付き、Timeline 切り替えのアニメーションと同じルールに従います。
struct TotalCaffeineView: View {
    @Environment(\.levelOfDetail) var levelOfDetail
    
    let entry: CaffeineLogEntry

    var body: some View {
        VStack {
            Text("Total Caffeine")
                .font(.caption)

            Text(totalCaffeine.formatted())
                .font(caffeineFont)
        }
    }
    
    var caffeineFont: Font {
        if levelOfDetail == .simplified {
            .largeTitle
        } else {
            .title
        }
    }
    
    var totalCaffeine: Measurement<UnitMass> {
        entry.totalCaffeine
    }
}

ポイント:

  • caffeineFont computed property は levelOfDetail に応じて異なるフォントサイズを返します。simplified モードでは .largeTitle、default モードでは .title です。
  • 「サイズではなく距離に応じてレイアウトを切り替える」というこの考え方こそが、visionOS Widget 設計の核心です。

Live Activity を CarPlay に対応させる

Live Activity を CarPlay のメイン画面に表示させるには、明示的に宣言する必要があります(11:49)。

struct ShopOrderLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: Attributes.self) { context in
            ActivityView(context: context)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    ExpandedView(context: context)
                }
            } compactLeading: {
                LeadingView(context: context)
            } compactTrailing: {
                TrailingView(context: context)
            } minimal: {
                MinimalView(context: context)
            }
        }
        .supplementalActivityFamilies([.small])
    }
}

ポイント:

  • .supplementalActivityFamilies([.small]): この Live Activity が .small という補助ファミリーもサポートすることを宣言します。CarPlay のメイン画面や Apple Watch ではこのファミリーを使ってレンダリングされます。
  • この行を加えると同時に、ビュー層で @Environment(\.activityFamily) による分岐を行い、.small 専用の簡略化されたビューを別途用意する必要があります。

Relevance Widget: 時間帯に応じて表示する

watchOS 26 では RelevanceKit が導入されました。Timeline Widget と異なり、Relevance Widget は自分自身で「どの時間帯に関連するか」を宣言し、その情報をもとにシステムが Smart Stack に表示します(16:20)。

struct HappyHourRelevanceWidget: Widget {
    var body: some WidgetConfiguration {
        RelevanceConfiguration(
            kind: "HappyHour",
            provider: Provider()
        ) { entry in
            WidgetView(entry: entry)
        }
    }
}

ポイント:

  • RelevanceConfigurationStaticConfiguration AppIntentConfiguration と並ぶ、3 つ目の Widget 構成タイプです。
  • Provider は RelevanceEntriesProvider プロトコルを実装し、relevance() メソッドで WidgetRelevance を返すことで、現在「関連する時間帯」がどれであるかをシステムに伝えます。
  • 複数の attribute を同時に返すこともできます。たとえば 3 軒のカフェの happy hour を、Smart Stack 上に同時に 3 つのインスタンスとして並べることが可能です。

WidgetKit Push: サーバー側からの能動的なリフレッシュ

今回もっとも大きなエンジニアリング上の変更です。Widget は WidgetPushHandler を登録し、push token を受け取ったらサーバーへ送り返します(21:13)。

struct CaffeineTrackerPushHandler: WidgetPushHandler {
    func pushTokenDidChange(_ pushInfo: WidgetPushInfo, widgets: [WidgetInfo]) {
        // Send push token and subscription info to server
    }
}

ポイント:

  • WidgetPushHandler は Widget 専用に設計された新しいプロトコルで、アプリ本体の push token とは別物です。
  • pushTokenDidChange は token に変更があったときに呼ばれます。引数には、現在その Widget が設置されているすべての場所(home screen / lock screen / Smart Stack など)が含まれます。
  • token を受け取った後は、アプリ自身がサーバーへ送って保存する責任を持ちます。

サーバーから送るプッシュペイロードは非常にシンプルです(22:29)。

{
    "aps": {
        "content-changed": true
    }
}

ポイント:

  • content-changed: true は WidgetKit 専用のフィールドで、「データが変わったので新しい timeline を取りに行ってほしい」とシステムに伝えます。
  • プッシュ自体にデータは含まれず、トリガーされるのは Provider の次の timeline リクエストです。サーバーは「更新の必要がある」と通知するだけで、コンテンツを直接渡すわけではありません。
  • プッシュには予算上の制限があり、Timeline スケジュールとシステムのスロットルポリシーを共有します。1 秒ごとに変化するような高頻度データは push ではなく、Timeline 標準の定期リフレッシュを使うべきです。

重要な気づき

  1. 既存 Widget に accented mode 対応を加える: ユーザーが clear glass テーマに切り替えると、未対応の Widget は真っ白な塊になってしまいます。なぜ取り組む価値があるか: iOS 26 のホーム画面カスタマイズはすべてのユーザーに向けた機能であり、対応の普及は急速に進みます。どこから始めるか: Widget の画像リソースに .widgetAccentedRenderingMode(.desaturated) を付け、@Environment(\.widgetRenderingMode) で分岐して不要なグラデーションや装飾を非表示にします。シミュレータで accented mode を直接プレビューできます。

  2. 時効性の高いサーバーイベントを Push Widget Updates に切り替える: フライト状況、注文ステータス、試合のスコアなど「変化点はまばらだが即座に表示する必要がある」データが対象です。なぜ取り組む価値があるか: Timeline スケジュールには最小間隔の制限があり、これまでは事前予測しかできず、イベント発生時には表示が遅れていました。どこから始めるか: WidgetPushHandler を実装し、token を既存の APNs サービスへ送り、イベント発火地点で content-changed: true のサイレントプッシュを送るだけで完了です。ペイロードのコードは事実上書く必要がありません。

  3. 営業情報を扱うアプリに Relevance Widget を組み込む: レストランの happy hour、ジムのクラス時間、映画の上映時刻などです。なぜ取り組む価値があるか: ユーザーが手動で Widget を追加しなくても、関連する時間帯になればシステムが自動的に Smart Stack の文字盤へ送り出してくれます。どこから始めるか: StaticConfiguration の代わりに RelevanceConfiguration を使い、RelevanceEntriesProvider.relevance() で時間帯付きの WidgetRelevance を返します。複数の店舗は複数の attribute として共存させられます。

  4. iPad Widget をそのまま visionOS で活かす: もっともコストが低い拡張先です。なぜ取り組む価値があるか: visionOS 26 は iPad Widget を自動的にサポートするため、コードを書かずに空間デバイスへの入口を手に入れられます。どこから始めるか: まず visionOS シミュレータで既存 Widget を一通り動かし、遠距離での見え方を最適化したい画面に @Environment(\.levelOfDetail) を加えます。アルバム系・ポスター系のコンテンツであれば、.widgetTexture(.paper) を有効にするだけで一気に違った見え方になります。

  5. Live Activity に .supplementalActivityFamilies(.small) を 1 行追加する: なぜ取り組む価値があるか: 1 行のコードで CarPlay のメイン画面と Apple Watch の表示枠を同時に獲得できます。どこから始めるか: ActivityConfiguration の後ろに modifier を追加し、ビュー層で @Environment(\.activityFamily) を使って .small 用の簡略化されたビューを書きます。文字を大きく、アイコンは少なく、ボタンは置かない構成が基本です。


関連 Session

  • Bring Swift Charts to the third dimension — Swift Charts が 3D チャートに対応し、visionOS Widget と合わせて空間コンピューティングのビジュアライゼーションスタックを構成します。
  • Build an AppKit app with the new design — macOS Tahoe の新しいデザイン言語。Widget やメニューバー Control の出自にあたります。
  • Create icons with Icon Composer — Liquid Glass デザインシステムにおけるアイコン制作。Widget の accented mode と同じ系譜です。
  • Elevate the design of your iPad app — iPad アプリのデザインガイド。大画面の iPad 上で Widget をどう見せるかを考えるうえで重要な参考になります。

コメント

GitHub Issues · utterances