WidgetKit i iOS 26: Komplett Guide till Interaktiva Widgets, Control Center och Cross-Platform-utveckling
Praktisk guide till WidgetKit i iOS 26: bygg din första widget, gör den interaktiv med App Intents, publicera i Control Center och hantera skillnader mellan iPhone, iPad, Mac, Watch och Vision Pro.
WidgetKit är Apples ramverk för att bygga widgets som visas på hemskärmen, låsskärmen, i StandBy-läget, i Notification Center på macOS, på Apple Watch-urtavlan och (sedan iOS 18) i Control Center. I iOS 26 har WidgetKit fått bättre stöd för interaktivitet via App Intents, förfinade Control Center-kontroller, uppdaterade widgetstorlekar för visionOS 26 och stramare minnesregler. Den här guiden går igenom hela stacken: hur du bygger en widget, gör den interaktiv, publicerar den i Control Center och hanterar plattformsskillnader mellan iPhone, iPad, Mac, Watch och Vision Pro.
WidgetKit i iOS 26 kräver iOS 14 eller senare för grundläggande widgets, iOS 17 för interaktivitet och iOS 18 för Control Center-kontroller.
En widget består av tre delar: en TimelineProvider som levererar entries, en SwiftUI-vy som renderar dem och en Widget-konfiguration som binder ihop allt.
Interaktiva widgets bygger på App Intents. Bara Button och Toggle med ett intent:-argument fungerar; godtyckliga gester stöds inte.
Control Center-kontroller använder ControlWidget och delar samma App Intent-modell som Home Screen-widgets, vilket ger enorm kodåteranvändning.
Minnesbudgeten för widget-extensions är cirka 30 MB. Överskrids den kill:as processen utan varning, oavsett plattform.
Samma widget kan kompileras för iPhone, iPad, Mac (via Mac Catalyst eller native), watchOS och visionOS 26 med enbart plattformsspecifika supportedFamilies-anrop.
Hur fungerar WidgetKit-arkitekturen?
WidgetKit är ett deklarativt ramverk där du inte äger renderingsloopen. Systemet bestämmer när din widget ska visas och när dess vy ska ritas om. Din uppgift är att leverera en timeline, en ordnad lista av entries där varje entry innehåller ett datum och den data som ska visas då. Systemet väljer sedan rätt entry baserat på aktuell tid och renderar den associerade SwiftUI-vyn.
Den här designen är avsiktlig. Widgets körs inte kontinuerligt; de är statiska ögonblicksbilder som cachas av systemet och plockas fram vid behov. Det gör dem extremt strömsnåla, men innebär också att du inte kan köra godtycklig kod när användaren tittar på dem. All datahämtning måste ske i förväg när du bygger timelinen.
Tre huvudkomponenter samverkar:
TimelineProvider: ansvarar för att skapa placeholder-, snapshot- och timeline-entries. Det är här din datahämtning sker.
TimelineEntry: en enkel struct med ett date: Date och alla data widget-vyn behöver för att rendera sig själv.
Widget: deklarationen som binder ihop kind, konfiguration, provider och vy. Den registreras i en WidgetBundle.
På iOS 17 och senare rekommenderar Apple AppIntentTimelineProvider istället för klassiska IntentTimelineProvider. Den ersätter SiriKit intents med moderna App Intents och integrerar direkt med Apple Intelligence i iOS 26. Jag använder den konsekvent nedan, eftersom migreringen från gamla intents i ett tidigare projekt bara tog någon eftermiddag och sparade oss mycket felmeddelanden.
Vilka widgetfamiljer finns i iOS 26?
Widgetfamiljer bestämmer var din widget kan visas och i vilken storlek. iOS 26 stödjer följande värden av WidgetFamily:
Familj
Plattformar
Placering
Ungefärlig storlek (iPhone 17 Pro)
systemSmall
iOS, iPadOS, macOS, visionOS
Hemskärm / skrivbord
170×170 pt
systemMedium
iOS, iPadOS, macOS, visionOS
Hemskärm / skrivbord
364×170 pt
systemLarge
iOS, iPadOS, macOS, visionOS
Hemskärm / skrivbord
364×382 pt
systemExtraLarge
iPadOS, macOS, visionOS
Hemskärm / skrivbord
780×382 pt (iPad)
accessoryCircular
iOS (låsskärm/StandBy), watchOS
Låsskärm, urtavla
76×76 pt
accessoryRectangular
iOS (låsskärm/StandBy), watchOS
Låsskärm, urtavla
172×76 pt
accessoryInline
iOS (låsskärm), watchOS
Ovanför klockan
Textrad
accessoryCorner
watchOS
Corner Complications
Rundad hörnkomplikation
Notera att systemExtraLarge inte finns på iPhone. Försöker du deklarera den där händer helt enkelt ingenting (Xcode varnar inte heller). På visionOS 26 renderas alla system-familjer som svävande fönster i användarens miljö, med subtila glaseffekter som passar Liquid Glass. Om du vill fördjupa dig i det designspråket har jag skrivit en separat guide om Liquid Glass i SwiftUI.
Bygga din första widget steg för steg
Så, låt oss bygga något konkret. Vi gör en väderwidget som visar aktuell temperatur för användarens plats. Skapa först ett nytt Widget Extension-mål i Xcode 26 (File → New → Target → Widget Extension) och avmarkera "Include Configuration App Intent" om du vill börja enkelt. Vi lägger till konfigurationen längre ner.
import WidgetKit
import SwiftUI
struct WeatherEntry: TimelineEntry {
let date: Date
let temperature: Measurement<UnitTemperature>
let condition: String
let city: String
}
struct WeatherProvider: TimelineProvider {
func placeholder(in context: Context) -> WeatherEntry {
WeatherEntry(
date: .now,
temperature: .init(value: 18, unit: .celsius),
condition: "Klart",
city: "Stockholm"
)
}
func getSnapshot(in context: Context, completion: @escaping (WeatherEntry) -> Void) {
completion(placeholder(in: context))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> Void) {
Task {
let current = try await WeatherService.shared.fetchCurrent()
let entry = WeatherEntry(
date: .now,
temperature: current.temperature,
condition: current.condition,
city: current.city
)
// Ladda om timelinen om 30 minuter
let refresh = Date.now.addingTimeInterval(30 * 60)
completion(Timeline(entries: [entry], policy: .after(refresh)))
}
}
}
Providern har tre metoder som systemet anropar vid olika tidpunkter. placeholder används medan datan laddas och i widgetgalleriet vid preview. getSnapshot anropas när Siri, Spotlight eller Smart Stack behöver en snabb ögonblicksbild. getTimeline anropas när systemet vill ha en ny lista med entries att visa.
Vyn är helt vanlig SwiftUI:
struct WeatherWidgetView: View {
let entry: WeatherEntry
@Environment(\.widgetFamily) private var family
var body: some View {
switch family {
case .systemSmall:
VStack(alignment: .leading) {
Text(entry.city).font(.caption).foregroundStyle(.secondary)
Spacer()
Text(entry.temperature.formatted(.measurement(width: .abbreviated)))
.font(.system(size: 32, weight: .bold, design: .rounded))
Text(entry.condition).font(.caption)
}
.padding()
case .accessoryCircular:
Text(entry.temperature.formatted(.measurement(width: .narrow)))
.font(.system(size: 14, weight: .bold))
default:
HStack {
Text(entry.city)
Spacer()
Text(entry.temperature.formatted())
}
.padding()
}
}
}
Slutligen bindningen:
struct WeatherWidget: Widget {
let kind: String = "WeatherWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: WeatherProvider()) { entry in
WeatherWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Väder")
.description("Aktuell temperatur för din plats.")
.supportedFamilies([.systemSmall, .systemMedium, .accessoryCircular, .accessoryRectangular])
}
}
Modifieraren containerBackground(_:for:) är obligatorisk från iOS 17. Utan den får du en tom bakgrund i StandBy och på låsskärmen. Använd .fill.tertiary som en säker default, eller egen färg för branding.
Hur gör man en widget interaktiv i iOS 26?
Interaktiva widgets introducerades i iOS 17 och bygger på App Intents. Regeln är strikt: enbart Button och Toggle med ett intent:-argument fungerar. Godtyckliga onTapGesture-modifierare eller NavigationLink ignoreras, eftersom widgets renderas out-of-process och systemet inte vet hur det ska serialisera generella closures.
Jag brände en halv arbetsdag på precis det här problemet när jag först provade. Så här ser rätt uppsättning ut.
Steg ett: definiera ett App Intent som utför handlingen. Om du inte har jobbat med App Intents tidigare rekommenderar jag min guide till App Intents i SwiftUI, som täcker grunderna.
import AppIntents
import WidgetKit
struct ToggleFavoriteIntent: AppIntent {
static var title: LocalizedStringResource = "Växla favorit"
@Parameter(title: "Artikel-ID")
var articleID: String
init() {}
init(articleID: String) { self.articleID = articleID }
func perform() async throws -> some IntentResult {
let store = FavoriteStore.shared
await store.toggle(articleID: articleID)
WidgetCenter.shared.reloadTimelines(ofKind: "FavoritesWidget")
return .result()
}
}
Steg två: använd intenten i en Button inuti widget-vyn.
struct FavoritesWidgetView: View {
let entry: FavoritesEntry
var body: some View {
VStack {
ForEach(entry.articles) { article in
HStack {
Text(article.title).lineLimit(1)
Spacer()
Button(intent: ToggleFavoriteIntent(articleID: article.id)) {
Image(systemName: article.isFavorite ? "star.fill" : "star")
}
.buttonStyle(.plain)
}
}
}
.padding()
}
}
När användaren trycker på stjärnan körs intenten i appens huvudprocess (eller i en dedikerad "background app launch"-kontext på macOS). Efter perform() returnerar anropar vi WidgetCenter.shared.reloadTimelines(ofKind:) för att tvinga en omladdning, så användaren ser resultatet inom några hundra millisekunder.
Hur lägger man till en widget i Control Center?
Från iOS 18 kan tredjepartsappar leverera egna Control Center-kontroller via typen ControlWidget. På iOS 26 har API:et stabiliserats och delar nu samma App Intent-modell som Home Screen-widgets, vilket gör kodåteranvändningen nästan total.
import AppIntents
import WidgetKit
import SwiftUI
struct QuickTimerControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "com.example.QuickTimer") {
ControlWidgetButton(action: StartTimerIntent(duration: 300)) {
Label("5 min timer", systemImage: "timer")
}
}
.displayName("Snabbtimer")
.description("Startar en 5-minuters timer med ett tryck.")
}
}
struct StartTimerIntent: AppIntent {
static var title: LocalizedStringResource = "Starta timer"
static var openAppWhenRun: Bool = false
@Parameter(title: "Sekunder")
var duration: Int
init() {}
init(duration: Int) { self.duration = duration }
func perform() async throws -> some IntentResult {
await TimerService.shared.start(duration: TimeInterval(duration))
return .result()
}
}
Registrera din kontroll i widget-bundlen precis som en vanlig widget:
@main
struct MyWidgetBundle: WidgetBundle {
var body: some Widget {
WeatherWidget()
QuickTimerControl()
}
}
Efter installation dyker kontrollen upp bland användarens tillgängliga kontroller i Control Center och kan även bindas till Action Button på iPhone 15 Pro och senare, eller till låsskärmens kontrollknappar från iOS 18 och framåt. Enligt Apples Human Interface Guidelines för controls bör kontrollen utföra en väl definierad handling. Motstå frestelsen att stoppa in flera relaterade funktioner i samma yta.
Plattformsskillnader: iPhone, iPad, Mac, Watch och visionOS
WidgetKit är kanske det mest genuint plattformsöverskridande ramverket Apple erbjuder. Samma kod kompilerar till fem plattformar, men det finns viktiga skillnader du måste hantera i praktiken.
iPhone och iPadOS
iPad stödjer systemExtraLarge (extra bred widget), vilket iPhone inte gör. iPad har också en dedikerad låsskärm sedan iPadOS 17 där accessory*-familjerna fungerar. Använd @Environment(\.widgetFamily) för att anpassa layouten och deklarera supportedFamilies plattformsspecifikt om det behövs.
macOS
På macOS bor widgets på skrivbordet (från macOS 14) och i Notification Center. Widgets kompilerade för iOS körs automatiskt på Apple Silicon-Macar via en Mac Catalyst-liknande brygga; en native macOS-version kan använda mus-hover-effekter via hoverEffect(_:). Watch-familjerna är inte tillgängliga.
watchOS
På watchOS ersätter widgets de klassiska Complications sedan watchOS 9. Använd accessoryCircular, accessoryRectangular, accessoryInline och accessoryCorner. En Smart Stack-widget kan aktivt föreslås av systemet baserat på tid och plats via RelevantContext.
visionOS 26
visionOS 26 renderar systemfamiljer som svävande 3D-fönster med subtil parallax. Använd ornament-liknande layouter försiktigt; texten måste vara läsbar på en meters avstånd. Från visionOS 26 stöds även widgetLevelOfDetail som anpassar innehåll beroende på hur nära användaren är fönstret. Det mönstret finns inte på någon annan plattform.
Timeline-strategier och batteriekonomi
WidgetKit stödjer tre TimelineReloadPolicy-värden:
.atEnd: ladda om när sista entryn i timelinen har passerat.
.after(Date): ladda om vid en specifik tidpunkt.
.never: ladda aldrig om automatiskt; be systemet göra det via WidgetCenter.shared.reloadTimelines(ofKind:).
Systemet garanterar inte att din widget uppdateras exakt när du bad om det. Apples dokumentation nämner en ungefärlig budget om 40–70 timeline-refreshes per widget per dag. Om användaren aldrig tittar på widgeten kan systemet välja att inte alls refresha den. Bygg därför tåligt: anta att din widget kan visa 2 timmar gamla data och rita en tydlig tidsstämpel om det är viktigt.
För push-drivna uppdateringar från servern (t.ex. sportresultat, aktiekurser) använd Apple Push Notification service med content-available: 1 och anropa reloadTimelines i appens push-handler. På iOS 26 finns även möjligheten att skicka background push direkt till widget-extensionen via reloadRelevances, vilket är betydligt strömsnålare.
Varför uppdateras inte min widget? Minne och prestanda
Den vanligaste orsaken till att en widget "slutar fungera" är att den överskred systemets minnesbudget och kill:ades utan varning. Widget-extensions har en hård gräns på cirka 30 MB residentminne, oavsett om enheten har 6 GB eller 16 GB RAM. Kryssar du över den gränsen dödas processen, och widgeten visar antingen sin sista cachade rendering eller en tom platshållare tills nästa refresh.
Praktiska konsekvenser:
Ladda aldrig fullstora bilder. Använd UIImage(named:) med förskalade assets, eller nedsampla med ImageIO.
Undvik tunga Core Data-graph-hämtningar; hämta bara det som behövs för entryn.
Bygg inte SwiftData-modeller med djupa relationer i widget-processen. Exportera platt DTO från huvudappen via App Groups istället. Se min SwiftData-guide för mönster som fungerar bra över processgränser.
Kör Instruments > Allocations mot Widget Extension-schemat innan release. Om peak-minnet överstiger 25 MB har du bara 5 MB marginal och kraschar då och då i produktion.
Andra vanliga orsaker till att widgets inte uppdateras: du glömde WidgetCenter.shared.reloadTimelines(ofKind:) efter en datamutation i appen, din TimelineReloadPolicy är .never, eller systemet har medvetet backat av på refresh-frekvensen på grund av låg batterinivå eller Low Power Mode.
Konfigurerbara widgets med AppIntentConfiguration
Om du vill låta användaren välja innehåll (t.ex. vilken stad väderwidgeten visar), använd AppIntentConfiguration istället för StaticConfiguration.
struct SelectCityIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Välj stad"
@Parameter(title: "Stad", default: "Stockholm")
var city: String
}
struct WeatherWidget: Widget {
let kind: String = "WeatherWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: SelectCityIntent.self,
provider: WeatherProvider()
) { entry in
WeatherWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.supportedFamilies([.systemSmall, .systemMedium])
}
}
Din TimelineProvider måste nu ändras till AppIntentTimelineProvider, där varje metod tar en instans av intenten som andra argument. Där ligger användarens val.
App Intents integreras dessutom med Apple Intelligence i iOS 26. Konfigurerade widgets kan visas som förslag i Siri och Spotlight utifrån användarens rutiner. Du får det gratis så länge du deklarerar meningsfulla title- och description-strängar.
Vad är skillnaden mellan en widget och en Live Activity?
En widget visar statisk data som uppdateras via en förbestämd timeline; en Live Activity uppdateras i realtid via ActivityKit och lever bara medan en händelse pågår (t.ex. en pågående match eller matleverans). Widgets är alltid tillgängliga; Live Activities visas bara aktivt. Läs min Live Activities-guide för fördjupning i den senare.
Kan widgets köra godtycklig kod när användaren trycker på dem?
Nej. Från iOS 17 fungerar bara Button och Toggle med App Intents som interaktivitet. Andra gester och navigationsmönster stöds inte eftersom widgeten renderas out-of-process och systemet inte kan serialisera generella closures.
Fungerar samma widget-kod på iPhone, Apple Watch och Vision Pro?
Ja, i grunden. Timeline-logiken kan delas via ett Swift-paket. Skillnaderna ligger i vilka WidgetFamily-värden respektive plattform stödjer och vilka UI-metaforer som passar (kanttexter på watchOS, svävande fönster på visionOS, hover-effekter på macOS).
Hur mycket minne får en widget använda?
Cirka 30 MB residentminne. Överskrids gränsen dödas widget-processen utan felmeddelande. Nedsampla bilder, undvik djupa objektgrafer och profilera i Instruments innan release.
Hur ofta uppdaterar iOS min widget?
Apple garanterar inget exakt tal, men riktvärdet är 40–70 timeline-refreshes per widget per dag. Systemet minskar frekvensen om användaren sällan tittar på widgeten, om batterinivån är låg eller Low Power Mode är aktivt. Använd push för händelsedrivna uppdateringar snarare än korta polling-intervall.
SensoryFeedback är SwiftUI:s deklarativa modifikator för haptik i iOS 26. Komplett guide till alla feedback-typer, Core Haptics för anpassade mönster, watchOS med WKHapticType, testning och tillgänglighet.
Bygg Live Activities i iOS 26 från grunden med ActivityKit, Dynamic Island och SwiftUI. Guide till token- och broadcast-push via APNs, App Intents, stale-läge, VoiceOver och throttling-felsökning med kod som fungerar direkt.
Skriv dina egna Swift-makron med SwiftSyntax i Swift 6.2 och Xcode 26. Genomgång av paketstruktur, freestanding vs attached, diagnostik och testning, med två kompletta exempel du kan köra direkt.