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

What’s new in widgets

Watch original video

Highlight

The WWDC25 widget update does three things: it expands widgets to visionOS and CarPlay, brings Relevance Widgets to watchOS so they appear during the right time window, and lets APNs push refresh widgets directly.


Core Content

For the past few years, widgets have been stuck on two problems. First, platform coverage was thin: visionOS had no native widgets, and CarPlay could only watch from the sidelines. Second, the only refresh mechanism was Timeline. Developers had to predict data hours ahead, or wait for the system to give them a turn under throttling. When the server had new data, there was no way to tell the widget directly.

WWDC25 tackles both problems together. Widgets, Live Activities, and Controls all land on visionOS 26, CarPlay, and the macOS Tahoe menu bar at once. Widgets on visionOS can stick to a wall or sit recessed in a wooden desk, and they switch to a simplified layout when viewed from across the room. CarPlay places widgets to the left of the dashboard in StandBy style. watchOS 26 adds the Relevance Widget — it declares “I’m only relevant during happy hour, 17:00–19:00,” and once that window passes it disappears from the Smart Stack. The biggest change is WidgetKit Push Notifications: the server sends a silent push with content-changed: true, and the widget refreshes immediately, no longer bound by the Timeline schedule.


Detailed Content

Accented Rendering Mode

The iOS 26 Home Screen adds clear glass and monochrome tint themes. The system first renders widget content in accented rendering mode (everything tinted white), then strips the background and replaces it with glass or a solid color. Plain widgets need no changes, but widgets that use photos or brand colors break — the image turns entirely white and becomes unreadable.

Apple’s answer is to watch the widgetRenderingMode environment value and branch the layout based on the current mode (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()
            }
        }
    }
}

Key points:

  • @Environment(\.widgetRenderingMode): reads which rendering mode the system is using, either .fullColor or .accented.
  • if renderingMode == .fullColor: in full-color mode, show the large image with a gradient overlay.
  • if renderingMode == .accented: in accented mode, switch layouts and put the image back above the text.
  • .widgetAccentedRenderingMode(.desaturated): keeps the grayscale detail of the image in accented mode instead of tinting it solid white. The modifier also accepts .accented, .accentedDesaturated, .fullColor, and nil. Use .fullColor for album art or book covers.

visionOS Widget Configuration

On visionOS 26, widgets can pin to walls or recess into desks (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])
    }
}

Key points:

  • .supportedMountingStyles([.elevated]): declares that only the “raised off the wall” mounting style is supported. The other option is .recessed (set into a wooden desk). The modifier also works on iOS.
  • .widgetTexture(.paper): visionOS only. Replaces the default glass material with a poster-paper texture.
  • .supportedFamilies([.systemExtraLargePortrait]): a new portrait extra-large size on visionOS 26. The original systemExtraLarge is the landscape version.

Level of Detail: Auto-Simplify at Distance

A visionOS widget can sit on the far wall of a room, where text gets small and buttons hard to tap. The new levelOfDetail environment value handles this (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()
            }
        }
    }
}

Key points:

  • levelOfDetail is either .default or .simplified, switched automatically by the system based on distance.
  • At a distance, hide LogDrinkView — buttons are hard to tap from far away, so drop them first.
  • Pair the distance switch with a larger font for TotalCaffeineView (see the next snippet) so the core “total caffeine” number stays readable.
  • Level-of-detail transitions get an animation, following the same rules as Timeline transitions.
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
    }
}

Key points:

  • The caffeineFont computed property returns a different size based on levelOfDetail.largeTitle in simplified mode, .title in default mode.
  • “Switch layout by distance, not by size” is the central idea behind visionOS widget design.

Live Activity on CarPlay

A Live Activity has to opt in to appear on the CarPlay Home Screen (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])
    }
}

Key points:

  • .supplementalActivityFamilies([.small]): declares that this Live Activity also supports the .small supplemental family. CarPlay Home Screen and Apple Watch render this family.
  • Along with this line, branch on @Environment(\.activityFamily) in the view layer and write a separate, stripped-down view for .small.

Relevance Widget: Show Up at the Right Time

watchOS 26 introduces RelevanceKit. Unlike a Timeline Widget, a Relevance Widget declares “I’m relevant during this window,” and the system shows it in the Smart Stack only then (16:20):

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

Key points:

  • RelevanceConfiguration is the third widget configuration type, alongside StaticConfiguration and AppIntentConfiguration.
  • The provider conforms to RelevanceEntriesProvider and returns a WidgetRelevance from relevance(), telling the system which “relevant windows” exist right now.
  • Multiple attributes can be returned at once — for example, the happy hours of three coffee shops can show up as three instances in the Smart Stack at the same time.

WidgetKit Push: Server-Driven Refresh

The biggest engineering change. The widget registers a WidgetPushHandler, gets a push token, and sends it back to the server (21:13):

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

Key points:

  • WidgetPushHandler is a new protocol made for widgets — its push token is separate from the app’s own.
  • pushTokenDidChange fires whenever the token changes. Its arguments include every place where the widget is currently installed (Home Screen, Lock Screen, Smart Stack, and so on).
  • Once the app has the token, it sends it to the server and stores it.

The server-side payload is minimal (22:29):

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

Key points:

  • content-changed: true is a WidgetKit-specific field that tells the system “the data changed, go fetch a new timeline.”
  • The push itself carries no data. It triggers the next timeline request from the provider — the server only signals “time to refresh,” it doesn’t ship content.
  • Pushes have a budget and share the system’s throttling policy with Timeline scheduling. Don’t use push for data that changes every second; use Timeline’s built-in periodic refresh instead.

Key Takeaways

  1. Add accented mode support to existing widgets: when a user switches to the clear glass theme, an unadapted widget turns into a white blob. Why it’s worth doing: Home Screen customization on iOS 26 is a feature for all users and adoption will move fast. How to start: tag the widget’s image assets with .widgetAccentedRenderingMode(.desaturated), then branch on @Environment(\.widgetRenderingMode) to hide gradients and decorations you don’t need. Preview accented mode directly in the simulator.

  2. Switch high-frequency server events to Push Widget Updates: flight status, order status, sports scores — data with sparse change points that need to be visible right away. Why it’s worth doing: Timeline scheduling has a minimum interval, so until now you had to predict ahead, and the screen always lagged behind real events. How to start: implement WidgetPushHandler, send the token to your existing APNs service, and fire a silent content-changed: true push at the moment of each event. The payload takes almost no code.

  3. Wire up a Relevance Widget for any business-hours app: restaurant happy hour, gym class times, movie showtimes. Why it’s worth doing: the user doesn’t have to add the widget by hand. The system pushes it onto the Smart Stack watch face during the relevant window. How to start: replace StaticConfiguration with RelevanceConfiguration, return a WidgetRelevance with the time window from RelevanceEntriesProvider.relevance(). Multiple business locations can coexist as multiple attributes.

  4. Reuse iPad widgets on visionOS: the lowest-cost expansion. Why it’s worth doing: visionOS 26 picks up iPad widgets automatically, so you get a foothold on a spatial device with zero code. How to start: run the existing widget in the visionOS simulator first; only the screens that need distance support need @Environment(\.levelOfDetail). For photo or poster content, turn on .widgetTexture(.paper) and the look changes immediately.

  5. Add one line — .supplementalActivityFamilies(.small) — to your Live Activity: why it’s worth doing: one line of code gets you a slot on both the CarPlay Home Screen and Apple Watch. How to start: append the modifier after ActivityConfiguration, then in the view layer branch on @Environment(\.activityFamily) and write a stripped-down view for .small — large text, few icons, no buttons.


Comments

GitHub Issues · utterances