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.fullColornilの 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
}
}
ポイント:
caffeineFontcomputed 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)
}
}
}
ポイント:
RelevanceConfigurationはStaticConfigurationAppIntentConfigurationと並ぶ、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 標準の定期リフレッシュを使うべきです。
重要な気づき
-
既存 Widget に accented mode 対応を加える: ユーザーが clear glass テーマに切り替えると、未対応の Widget は真っ白な塊になってしまいます。なぜ取り組む価値があるか: iOS 26 のホーム画面カスタマイズはすべてのユーザーに向けた機能であり、対応の普及は急速に進みます。どこから始めるか: Widget の画像リソースに
.widgetAccentedRenderingMode(.desaturated)を付け、@Environment(\.widgetRenderingMode)で分岐して不要なグラデーションや装飾を非表示にします。シミュレータで accented mode を直接プレビューできます。 -
時効性の高いサーバーイベントを Push Widget Updates に切り替える: フライト状況、注文ステータス、試合のスコアなど「変化点はまばらだが即座に表示する必要がある」データが対象です。なぜ取り組む価値があるか: Timeline スケジュールには最小間隔の制限があり、これまでは事前予測しかできず、イベント発生時には表示が遅れていました。どこから始めるか:
WidgetPushHandlerを実装し、token を既存の APNs サービスへ送り、イベント発火地点でcontent-changed: trueのサイレントプッシュを送るだけで完了です。ペイロードのコードは事実上書く必要がありません。 -
営業情報を扱うアプリに Relevance Widget を組み込む: レストランの happy hour、ジムのクラス時間、映画の上映時刻などです。なぜ取り組む価値があるか: ユーザーが手動で Widget を追加しなくても、関連する時間帯になればシステムが自動的に Smart Stack の文字盤へ送り出してくれます。どこから始めるか:
StaticConfigurationの代わりにRelevanceConfigurationを使い、RelevanceEntriesProvider.relevance()で時間帯付きのWidgetRelevanceを返します。複数の店舗は複数の attribute として共存させられます。 -
iPad Widget をそのまま visionOS で活かす: もっともコストが低い拡張先です。なぜ取り組む価値があるか: visionOS 26 は iPad Widget を自動的にサポートするため、コードを書かずに空間デバイスへの入口を手に入れられます。どこから始めるか: まず visionOS シミュレータで既存 Widget を一通り動かし、遠距離での見え方を最適化したい画面に
@Environment(\.levelOfDetail)を加えます。アルバム系・ポスター系のコンテンツであれば、.widgetTexture(.paper)を有効にするだけで一気に違った見え方になります。 -
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