WidgetKit in iOS 26: Push Reloads, Glass Rendering, and RelevanceKit
You built a widget, shipped it, and watched the timeline budget slowly strangle your refresh rate. Users saw stale data for minutes — sometimes hours — because WidgetKit’s pull-based reload system could never guarantee timeliness. iOS 26 rewrites the contract: your server pushes content directly to the widget, Liquid Glass reshapes how widgets render, and RelevanceKit gives you actual control over Smart Stack ranking on Apple Watch.
This post covers the four major WidgetKit changes in iOS 26: push-driven reloads via APNs, widgetAccentedRenderingMode
for Liquid Glass adaptation, the RelevanceKit framework for Smart Stack intelligence, and multi-state interactive
controls. We assume you already understand
WidgetKit timelines, widget families, and Live Activities and are
comfortable with push notification delivery and APNs configuration. We will not
cover ActivityKit’s iOS 26 expansion — that is covered in ActivityKit in iOS 26.
Contents
- The Problem
- Push-Driven Widget Reloads
- Liquid Glass Rendering for Widgets
- RelevanceKit: Smart Stack Ranking
- Multi-State Interactive Controls
- Advanced Usage
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Timeline-based widgets work well for content that changes predictably — a daily Pixar movie recommendation that rotates at midnight, a countdown to the next Pixar film release. But they fall apart for event-driven data. Consider a Pixar Movie Night app that lets a group of friends vote on which film to watch. Votes arrive in real time from your server. Under the old model, your only options were:
// Option A: Poll aggressively — burns through your timeline budget
let nextRefresh = Calendar.current.date(byAdding: .minute, value: 5, to: .now)!
let timeline = Timeline(entries: entries, policy: .after(nextRefresh))
// Option B: Wait for the app to reload timelines — user must open the app first
WidgetCenter.shared.reloadTimelines(ofKind: "MovieNightVoteWidget")
Option A wastes battery and still shows data up to five minutes stale. Option B defeats the purpose of a widget — the user has to open the app for the widget to update, which is backwards.
The system imposes a daily reload budget on every widget. Once you exhaust it, your timeline requests are silently deferred. There was no way to tell the system “this update is urgent” versus “this is a background refresh.” Every reload cost the same budget.
iOS 26 introduces a third path: push-driven reloads. Your server sends a lightweight APNs payload, and the system wakes
your TimelineProvider immediately, outside the normal budget. The widget updates within seconds, not minutes.
Push-Driven Widget Reloads
Apple Docs:
WidgetKit— WidgetKit framework
Push-driven widget reloads use the same APNs infrastructure you already have for push notifications. The payload targets your widget extension directly — your main app process is not launched. This is the single most impactful WidgetKit change since interactive widgets in iOS 17.
Opting In to Push Reloads
To enable push reloads, your widget must declare push support in its configuration. This is analogous to how Live
Activities opt into push updates with .pushType: .token, but the mechanism is different — widgets use a topic-based
APNs channel rather than per-instance tokens.
import WidgetKit
import SwiftUI
@available(iOS 26, *)
struct MovieNightWidget: Widget {
let kind: String = "MovieNightVoteWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: MovieNightConfigIntent.self,
provider: MovieNightTimelineProvider()
) { entry in
MovieNightWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.supportedFamilies([.systemSmall, .systemMedium])
.configurationDisplayName("Movie Night Votes")
.description("Live vote counts for tonight's Pixar film pick.")
.pushReloadsEnabled() // ← Opt into push-driven reloads
}
}
The .pushReloadsEnabled() modifier registers your widget kind with the system as push-reload-capable. The system
generates a topic string derived from your bundle identifier and widget kind, which you use as the APNs topic when
sending pushes from your server.
The APNs Payload
Push-driven widget reloads use a specific APNs push type. The payload is intentionally minimal — it signals the system
to invoke your TimelineProvider, not to deliver content directly. All data fetching still happens in your provider.
{
"aps": {
"content-available": 1,
"timestamp": 1750000000
}
}
The APNs request requires two headers that distinguish widget pushes from regular notifications:
apns-push-type: background— tells APNs this is a background delivery.apns-topic: <bundle-id>.push-type.widgets— routes the push to your widget extension. Replace<bundle-id>with your widget extension’s bundle identifier.
The timestamp field is mandatory. The system uses it to deduplicate pushes and discard stale ones. If two pushes
arrive with the same timestamp, the system drops the duplicate. Always use the current Unix epoch time.
Handling Push Reloads in the TimelineProvider
When a push reload arrives, the system calls your getTimeline(in:completion:) method. There is no new method to
implement — your existing provider works as-is. The difference is that the call is triggered by your server instead of
by the system’s budget scheduler.
@available(iOS 26, *)
struct MovieNightTimelineProvider: AppIntentTimelineProvider {
typealias Entry = MovieNightEntry
typealias Intent = MovieNightConfigIntent
func placeholder(in context: Context) -> MovieNightEntry {
.placeholder
}
func snapshot(for configuration: Intent, in context: Context) async -> MovieNightEntry {
await fetchCurrentVotes()
}
func timeline(for configuration: Intent, in context: Context) async -> Timeline<MovieNightEntry> {
let entry = await fetchCurrentVotes()
// Use .never — we rely on push reloads for updates, not polling
return Timeline(entries: [entry], policy: .never)
}
private func fetchCurrentVotes() async -> MovieNightEntry {
let votes = (try? await MovieNightAPI.shared.fetchVotes()) ?? []
return MovieNightEntry(date: .now, votes: votes)
}
}
Notice the reload policy is .never. This is deliberate. When push reloads handle all updates, there is no reason to
consume timeline budget with polling. The system calls timeline(for:in:) only when your server sends a push, or when
the user manually adds or re-adds the widget.
Tip: Push reloads do not count against your daily timeline reload budget. This is the key advantage — you get timely updates without burning through the system-imposed limit. However, Apple reserves the right to throttle push reloads if your server sends them excessively. Treat them like push notifications: send when data actually changes, not on a timer.
Server-Side Integration
On your server, send the push using the same APNs HTTP/2 API you use for notifications. Here is a conceptual outline — actual implementation depends on your backend stack:
// Simplified server-side logic (e.g., in a Vapor route handler)
func sendWidgetPushReload(deviceToken: String) async throws {
let payload = """
{
"aps": {
"content-available": 1,
"timestamp": \(Int(Date().timeIntervalSince1970))
}
}
"""
// Send to APNs with the widget-specific topic
try await apnsClient.send(
payload: payload,
to: deviceToken,
topic: "com.pixartracker.widget-extension.push-type.widgets",
pushType: .background,
priority: .low
)
}
Use priority .low (5) rather than .high (10). Widget reloads are not user-facing alerts — they are background
content updates. APNs may throttle or reject high-priority pushes that do not display an alert.
Liquid Glass Rendering for Widgets
Apple Docs:
widgetAccentedRenderingMode— WidgetKit
iOS 26’s Liquid Glass design system does not skip widgets. The system now renders home screen widgets with a translucent glass backing that blurs and tints based on the wallpaper behind them. This is not optional — if you do nothing, your widget gets the glass treatment automatically, and the results may not be what you want.
The widgetAccentedRenderingMode Environment Value
The new widgetAccentedRenderingMode environment value tells you how the system is currently rendering your widget.
This is the widget-level analog of widgetRenderingMode that was introduced for Lock Screen widgets in iOS 16, but it
specifically addresses the Liquid Glass accent behavior.
@available(iOS 26, *)
struct MovieNightWidgetView: View {
var entry: MovieNightEntry
@Environment(\.widgetAccentedRenderingMode) var accentedMode
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Movie Night")
.font(.headline)
.foregroundStyle(accentedMode == .accented ? .primary : .white)
ForEach(entry.votes.prefix(3)) { vote in
HStack {
Text(vote.filmTitle)
.font(.subheadline)
Spacer()
Text("\(vote.count)")
.monospacedDigit()
.fontWeight(.bold)
.foregroundStyle(voteCountColor)
}
}
}
.padding()
}
private var voteCountColor: Color {
switch accentedMode {
case .accented:
return .accentColor // System tint that harmonizes with glass
case .fullColor:
return .orange
@unknown default:
return .primary
}
}
}
When accentedMode is .accented, the system expects your content to use two-tone rendering: primary content in the
standard label color, and accent elements using .accentColor. The system tints .accentColor to match the wallpaper
and glass backing automatically. When the mode is .fullColor, you have full control over your palette.
Adapting Background and Contrast
The glass backing changes the contrast equation. A dark background that looked great on a solid wallpaper may become
unreadable when the glass effect lightens it. The safest approach is to rely on containerBackground with semantic
fills:
@available(iOS 26, *)
struct MovieNightGlassWidgetView: View {
var entry: MovieNightEntry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Widget content here
Text("Tonight's Pick")
.font(.headline)
if let topFilm = entry.votes.first {
Text(topFilm.filmTitle)
.font(.title2)
.fontWeight(.bold)
}
}
.padding()
.containerBackground(for: .widget) {
// Use a semantic material instead of a hard-coded color
// The system composites this with the Liquid Glass layer
Color.clear // Let glass handle the background entirely
}
}
}
Warning: Avoid opaque background colors in your
containerBackgroundon iOS 26. An opaque fill fights the glass effect and produces a visual artifact where the glass blurs the wallpaper and then your color covers it entirely. If you need a tinted background, use a translucent material like.ultraThinMaterialor a color with reduced opacity.
Testing Glass Rendering
In Xcode 26, the widget preview canvas includes a wallpaper picker that lets you test your widget against different backgrounds. Use it. A widget that looks perfect on a dark wallpaper may be unreadable on a light one. Test at minimum against a dark photo, a light gradient, and a high-contrast pattern.
RelevanceKit: Smart Stack Ranking
Apple Docs:
RelevanceKit— RelevanceKit framework
On Apple Watch, the Smart Stack ranks widgets based on predicted relevance — time of day, user habits, and opaque system heuristics. Before iOS 26, you had almost no influence over this ranking. Your widget either appeared or it did not, and you could not tell the system “this widget is relevant right now because something just happened.”
RelevanceKit changes this. It provides a structured API for declaring relevance signals that the Smart Stack ranking algorithm considers alongside its own heuristics.
Declaring Relevance
The core API is RelevanceProvider, a protocol your widget extension adopts to supply relevance signals:
import RelevanceKit
import WidgetKit
@available(iOS 26, watchOS 12, *)
struct MovieNightRelevanceProvider: RelevanceProvider {
func relevance() async -> WidgetRelevance<MovieNightConfigIntent> {
let hasActiveSession = await MovieNightAPI.shared.hasActiveVotingSession()
let lastVoteTime = await MovieNightAPI.shared.lastVoteTimestamp()
// Higher score = more relevant. Range is 0.0 to 1.0.
let score: Double = hasActiveSession ? 0.9 : 0.1
return WidgetRelevance(
configuration: MovieNightConfigIntent(),
score: score,
duration: .hours(2) // Relevance signal expires after 2 hours
)
}
}
The score is a hint, not a guarantee. The system weighs it against user behavior patterns. A widget the user has never
tapped will not jump to the top of the Smart Stack just because you returned a 1.0 score. But a widget the user engages
with regularly will benefit significantly from accurate relevance signals during active moments.
Contextual Relevance with TimeRelevance
For widgets whose relevance is predictable based on time, RelevanceKit supports TimeRelevance entries — a series of
time-score pairs similar to a timeline:
@available(iOS 26, watchOS 12, *)
struct MovieNightTimeRelevanceProvider: RelevanceProvider {
func relevance() async -> WidgetRelevance<MovieNightConfigIntent> {
// Movie night is every Friday from 7 PM to 11 PM
let calendar = Calendar.current
var relevanceEntries: [TimeRelevanceEntry] = []
// Build relevance for the next 4 Fridays
for weekOffset in 0..<4 {
if let friday = calendar.nextDate(
after: .now,
matching: DateComponents(weekday: 6, hour: 19),
matchingPolicy: .nextTime
) {
let start = calendar.date(
byAdding: .weekOfYear,
value: weekOffset,
to: friday
)!
let end = calendar.date(
byAdding: .hour,
value: 4,
to: start
)!
relevanceEntries.append(
TimeRelevanceEntry(date: start, score: 0.95)
)
relevanceEntries.append(
TimeRelevanceEntry(date: end, score: 0.1)
)
}
}
return WidgetRelevance(
configuration: MovieNightConfigIntent(),
timeRelevance: relevanceEntries
)
}
}
This tells the system “this widget is highly relevant on Friday evenings and mostly irrelevant the rest of the week.” The Smart Stack algorithm uses these signals to surface your widget proactively — the user glances at their watch at 7:15 PM on Friday and sees the vote counts without scrolling.
Registering the Provider
Connect your relevance provider to your widget by adding it in the widget bundle:
@available(iOS 26, watchOS 12, *)
@main
struct MovieNightWidgetBundle: WidgetBundle {
var body: some Widget {
MovieNightWidget()
.relevanceProvider(MovieNightRelevanceProvider())
}
}
Note: RelevanceKit is available on watchOS 12 and iOS 26. On iOS, it currently affects the Smart Stack behavior for StandBy mode and the Home Screen widget suggestions. On watchOS, it directly influences the watch face Smart Stack.
Multi-State Interactive Controls
iOS 17 introduced Button and Toggle in widgets via AppIntent. iOS 26 extends this with multi-state interactive
controls — widgets that cycle through more than two states with a single tap, without launching the app.
The Use Case
A simple toggle covers “watched / not watched.” But what about a Pixar movie night voting widget where each film can be in one of three states: no vote, upvote, or downvote? Before iOS 26, this required either multiple buttons (cluttered) or launching the app (defeats the purpose).
Implementing Multi-State Controls
Multi-state controls use a new AppIntent modifier that cycles through an enumeration of states:
import AppIntents
import WidgetKit
@available(iOS 26, *)
enum VoteState: String, AppEnum {
case none
case upvote
case downvote
static var typeDisplayRepresentation = TypeDisplayRepresentation(
name: "Vote State"
)
static var caseDisplayRepresentations: [VoteState: DisplayRepresentation] = [
.none: "No Vote",
.upvote: "Upvote",
.downvote: "Downvote"
]
}
@available(iOS 26, *)
struct CycleVoteIntent: AppIntent {
static var title: LocalizedStringResource = "Cycle Vote"
@Parameter(title: "Film ID")
var filmID: String
@Parameter(title: "Current State")
var currentState: VoteState
func perform() async throws -> some IntentResult {
let nextState: VoteState = switch currentState {
case .none: .upvote
case .upvote: .downvote
case .downvote: .none
}
await MovieNightAPI.shared.updateVote(
filmID: filmID,
state: nextState
)
return .result()
}
}
Use this intent in the widget view with the cycling button pattern:
@available(iOS 26, *)
struct VoteButtonView: View {
let filmID: String
let filmTitle: String
let currentVote: VoteState
var body: some View {
Button(
intent: CycleVoteIntent(
filmID: filmID,
currentState: currentVote
)
) {
HStack(spacing: 4) {
Image(systemName: voteIcon)
.foregroundStyle(voteColor)
Text(filmTitle)
.lineLimit(1)
}
}
.buttonStyle(.plain)
}
private var voteIcon: String {
switch currentVote {
case .none: "circle"
case .upvote: "hand.thumbsup.fill"
case .downvote: "hand.thumbsdown.fill"
}
}
private var voteColor: Color {
switch currentVote {
case .none: .secondary
case .upvote: .green
case .downvote: .red
}
}
}
Each tap fires the intent, which cycles to the next state and triggers a timeline reload. The widget re-renders with the updated state. The entire interaction completes without launching the app.
Tip: Pair multi-state controls with push reloads for the best user experience. When a user taps a vote button, the intent sends the vote to your server. Your server processes it, updates the aggregate counts, and sends a push reload to all participants’ widgets. Everyone sees the updated totals within seconds.
Advanced Usage
Combining Push Reloads with Fallback Polling
Push reloads depend on network connectivity and APNs availability. A robust widget should not rely exclusively on pushes. Use push reloads as the primary update mechanism but set a fallback polling interval as a safety net:
@available(iOS 26, *)
func timeline(
for configuration: MovieNightConfigIntent,
in context: Context
) async -> Timeline<MovieNightEntry> {
let entry = await fetchCurrentVotes()
// Push reloads handle real-time updates, but poll every 30 minutes
// as a fallback in case pushes are delayed or the device is offline
let fallbackDate = Calendar.current.date(
byAdding: .minute, value: 30, to: .now
)!
return Timeline(entries: [entry], policy: .after(fallbackDate))
}
This hybrid approach gives you the best of both worlds: sub-second updates when pushes arrive, and guaranteed freshness within 30 minutes even if they do not.
Glass Rendering with Custom Tints
If your brand demands a specific accent color rather than the system-derived tint, you can declare a preferred accent that the glass system will attempt to honor:
@available(iOS 26, *)
struct MovieNightAccentWidget: Widget {
let kind: String = "MovieNightVoteWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: MovieNightConfigIntent.self,
provider: MovieNightTimelineProvider()
) { entry in
MovieNightWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
.widgetAccentable() // ← Marks widget as accent-aware
}
.supportedFamilies([.systemSmall, .systemMedium])
.configurationDisplayName("Movie Night Votes")
.description("Live vote counts for tonight's Pixar film pick.")
.pushReloadsEnabled()
}
}
The .widgetAccentable() modifier tells the system that your widget is designed for the accented rendering path.
Without it, the system may default to full-color rendering, which bypasses the Liquid Glass tinting and can look
inconsistent with other widgets on the home screen.
RelevanceKit with User Activity Signals
Beyond time-based relevance, you can signal relevance based on user activity in your main app. When the user opens the movie night session in your app, donate a relevance signal:
import RelevanceKit
@available(iOS 26, *)
func userOpenedMovieNightSession() {
// Donate an immediate relevance boost from the main app
RelevanceKit.donateRelevance(
forWidgetKind: "MovieNightVoteWidget",
score: 0.95,
duration: .hours(4)
)
}
This tells the system “the user just engaged with the content this widget displays, so the widget is probably relevant for the next few hours.” The Smart Stack will factor this into its ranking decisions.
Warning: Do not donate high relevance scores indiscriminately. If every app launch donates a 1.0 score, the signal becomes meaningless and the system will learn to discount it. Only donate when the user’s action genuinely correlates with widget relevance.
Performance Considerations
Push Reload Budgets
Push reloads bypass the daily timeline reload budget, but they are not unlimited. Apple has not published a hard cap, but the system applies a throttle curve based on:
- Push frequency — Widgets receiving more than approximately 40-50 push reloads per day may see delays on subsequent pushes.
- Device state — Low Power Mode and Do Not Disturb can defer push reloads.
- Network conditions — Push reloads require a network path to APNs. Offline devices queue reloads but deliver them on reconnection.
Design your server to batch changes. If five votes arrive within 10 seconds, send one push after the batch settles, not five individual pushes.
Glass Rendering Overhead
Liquid Glass rendering adds compositing work to every widget render. In practice, the system handles this efficiently because widgets are rendered once and cached as bitmaps. However, avoid these patterns that force expensive re-renders:
- Animated content — Widgets do not support real-time animation, but
Textdate formatters like.timerand.relativecause periodic re-renders. Each re-render goes through the glass compositing pipeline. - High-resolution images — The glass blur interacts with the alpha channel of your content. Large images with complex alpha masks increase compositing time. Keep images small and use opaque assets where possible.
- Deep view hierarchies — Flatten your widget views. The glass effect applies to the entire widget as a unit, so deeply nested stacks add layout cost without visual benefit.
RelevanceKit Signal Processing
RelevanceKit signals are processed asynchronously by the system. Calling relevance() on your provider is not on the
critical path for widget rendering — it runs on a background schedule. Keep your relevance computation lightweight
(avoid network calls inside the provider if possible) and prefer TimeRelevance entries for predictable patterns, as
they are processed once and cached.
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Server-driven content (chat, voting, scores) | Push reloads — the primary use case they were designed for |
| Predictable schedule (daily quote, weekly summary) | Timeline with .after(date) is still simpler and sufficient |
| Widget on Apple Watch Smart Stack | Adopt RelevanceKit with time-based signals for best ranking |
| Custom brand palette | Use widgetAccentedRenderingMode to detect glass mode and adapt |
| More than two interactive states | Multi-state controls with cycling AppIntent |
| High-frequency updates (every few seconds) | Avoid push reloads — they will be throttled. Use Live Activities |
| Must support iOS 17/18 | Use @available checks and fall back to timeline-based reloads |
Summary
- Push-driven widget reloads bypass the daily timeline budget and deliver server-triggered updates within seconds.
Enable with
.pushReloadsEnabled()and send a lightweight APNs payload withapns-topic: <bundle-id>.push-type.widgets. - Liquid Glass rendering is automatic on iOS 26. Use
widgetAccentedRenderingModeto detect the current rendering mode and adapt your colors. Avoid opaque backgrounds that fight the glass compositing. - RelevanceKit gives you influence over Apple Watch Smart Stack ranking through
RelevanceProviderandTimeRelevanceentries. Higher scores during contextually relevant moments surface your widget when it matters. - Multi-state interactive controls extend iOS 17’s
AppIntent-powered buttons to cycle through anAppEnumof states, enabling three-way or multi-way interactions without launching the app. - Combine push reloads with a fallback
.after(date)policy for resilience against network issues and APNs delays.
The natural companion to these widget updates is the Liquid Glass design system itself — understanding how glass materials, tinting, and morphing transitions work across the entire UI will help you build widgets that feel native to iOS 26 rather than bolted on. Dive deeper in Liquid Glass Design System.