Live Activities i iOS 26: Komplett Guide till ActivityKit, Dynamic Island och Broadcast Push

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.

Live Activities iOS 26: Guide (2026)

Uppdaterad: 13 juli 2026

Live Activities är iOS-vyer som förblir fästa på låsskärmen och i Dynamic Island medan en pågående händelse (en resa, en leverans eller en timer) utvecklas i realtid, och i iOS 26 uppdateras de antingen från din app, via APNs push-token eller genom Apples nya broadcast-kanaler. Jag har byggt Live Activities sedan iOS 16.1 och tänker gå igenom hela stacken: ActivityAttributes, widget extension, Dynamic Island-regioner, push-flödet och de fällor som fortfarande fäller de flesta implementationer år 2026. Så, VoiceOver-beteende ingår genomgående, för det bygger jag alltid in från början.

  • Live Activities kräver en Widget Extension med en ActivityConfiguration, plus ActivityAttributes och en ContentState i huvudappen.
  • Dynamic Island renderas i tre lägen (compact, minimal och expanded), och du måste designa alla tre. Systemet väljer automatiskt.
  • Push-uppdateringar går via APNs push-type liveactivity med en unik token per aktivitet som kan roteras när som helst.
  • Frekventa uppdateringar kräver NSSupportsLiveActivitiesFrequentUpdates i Info.plist plus prioritet 5 för att undvika throttling.
  • iOS 26 introducerar broadcast-APNs där en enda push når alla aktivitetsinstanser, vilket är perfekt för sportmatcher och gate-ändringar.
  • En Live Activity varar max 8 timmar aktivt och 4 timmar i stale-läge. Total livslängd blir 12 timmar innan systemet tar bort den.

Vad är en Live Activity i iOS 26?

En Live Activity är en tidsbegränsad vy som visar realtidsdata om en pågående händelse, renderas med SwiftUI inuti en Widget Extension och hanteras av ActivityKit-ramverket. Till skillnad från en vanlig widget som uppdateras enligt systemets tidslinje, existerar en Live Activity bara medan händelsen pågår (leveransen som kommer, timern som tickar, matchen som spelas) och plockas sedan bort. Mentalmodellen är "tillfällig, glanceable, kopplad till en händelse".

I iOS 26 har Apple gjort tre saker som förändrar hur du bygger dem. För det första plockar Live Activities upp Liquid Glass-materialet och accented rendering mode via WidgetRenderingMode. För det andra har APNs fått en broadcast-kanal så du kan uppdatera tusentals aktiviteter med en enda push. För det tredje visas Live Activities nu även på macOS 26 och watchOS 11 på parade enheter, utan att du behöver skriva någon extra kod, men bara om din layout klarar det.

Hårdvarukraven är fortfarande skarpa. Dynamic Island fungerar på iPhone 14 Pro och senare, medan låsskärmspresentationen når alla iPhones med iOS 16.1 eller nyare. Simulatorn stöder Dynamic Island från iOS 18 och framåt, men jag testar alltid på riktig hårdvara innan release. Beteendet i StandBy, i Always On-läge och när enheten är låst med brytare skiljer sig från simulatorn.

Skapa Widget Extension och ActivityAttributes

Live Activities lever i en Widget Extension, inte i huvudappen. I Xcode: File > New > Target > Widget Extension, ge den ett namn som DeliveryActivity, och bocka INTE i "Include Configuration Intent". Den checkboxen är för vanliga widgets. Se sedan till att extension-target och app-target båda tillhör samma App Group, så du kan dela data mellan dem via UserDefaults(suiteName:) eller filsystemet.

Innan du kan bygga något behöver du en modell. ActivityAttributes är strukturen som beskriver Live Activity-innehållet, uppdelat i statiska fält (satta vid start, kan inte ändras) och en inbäddad ContentState med de dynamiska fälten som får uppdateras. Här är en leverans-tracker jag använder som skelett i mina projekt:

import ActivityKit
import Foundation

struct DeliveryAttributes: ActivityAttributes {
    // Dynamiska fält, de enda som kan uppdateras
    public struct ContentState: Codable, Hashable {
        var etaMinutes: Int
        var status: DeliveryStatus
        var driverName: String
        var lastUpdated: Date
    }

    // Statiska fält, sätts vid start och är oföränderliga
    let orderId: String
    let restaurantName: String
    let totalItems: Int
}

enum DeliveryStatus: String, Codable, Hashable {
    case preparing, onTheWay, arrived
}

Regeln jag alltid följer: allt som kan förändras under aktivitetens livstid ska ligga i ContentState. Om du glömmer ett fält där kommer du att upptäcka det när din UI vägrar uppdatera vid push-anrop tre timmar in i en debug-session (jag hade exakt den buggen i min senaste leveransapp, och den tog två kvällar att spåra). Lägg också in en lastUpdated: Date. Den gör det trivialt att räkna ut om innehållet är stale utan att hoppa mellan tidszoner.

Konfigurera Dynamic Island i SwiftUI

All rendering bor i Widget Extension i en ActivityConfiguration. Där definierar du både låsskärmsvyn och de tre Dynamic Island-lägena: compact (den mest normala varianten, smal och centrerad), minimal (när två aktiviteter delar utrymme och din trycks ut till pillret vid TrueDepth-kameran) och expanded (när användaren långtrycker). Systemet väljer, du får inte välja åt det.

import ActivityKit
import SwiftUI
import WidgetKit

struct DeliveryLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DeliveryAttributes.self) { context in
            // Låsskärmspresentation
            LockScreenDeliveryView(context: context)
                .activityBackgroundTint(Color.black.opacity(0.4))
                .activitySystemActionForegroundColor(Color.white)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    Label(context.attributes.restaurantName,
                          systemImage: "bag.fill")
                        .font(.caption)
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("\(context.state.etaMinutes) min")
                        .font(.title2.monospacedDigit())
                        .accessibilityLabel("Beräknad ankomst om \(context.state.etaMinutes) minuter")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    ProgressView(value: progress(for: context.state.status))
                        .tint(.green)
                }
            } compactLeading: {
                Image(systemName: "bag.fill")
            } compactTrailing: {
                Text("\(context.state.etaMinutes)m")
                    .monospacedDigit()
            } minimal: {
                Image(systemName: "bag.fill")
            }
            .widgetURL(URL(string: "myapp://order/\(context.attributes.orderId)"))
            .keylineTint(.green)
        }
    }

    private func progress(for status: DeliveryStatus) -> Double {
        switch status {
        case .preparing:  return 0.25
        case .onTheWay:   return 0.65
        case .arrived:    return 1.0
        }
    }
}

Fyra saker jag alltid rättar när jag reviewar en junior-implementation. (1) Glöm inte monospacedDigit(), annars kommer texten att skaka när siffror byts. (2) Sätt widgetURL på hela DynamicIsland-blocket, inte på enskilda vyer, om du vill ha ett enhetligt tryckbeteende. (3) keylineTint styr den tunna färgade ringen runt Dynamic Island i expanded-läget. Använd en färg som klarar både ljust och mörkt läge. (4) Compact-lägena har extremt lite utrymme. Testa "5m" och "125m" båda innan release, annars klipps texten.

Starta, uppdatera och avsluta aktiviteten

Här är den vanligaste felkällan jag ser: att anropa Activity.request utan att först kontrollera areActivitiesEnabled. Användaren kan ha stängt av Live Activities i inställningarna, eller så har systemet redan nått sin gräns. Kolla först, hantera fel, och håll en referens till aktiviteten så du kan uppdatera den senare.

import ActivityKit
import Foundation

@MainActor
final class DeliveryActivityManager {
    private var activity: Activity<DeliveryAttributes>?

    func start(orderId: String, restaurantName: String, totalItems: Int) async throws {
        guard ActivityAuthorizationInfo().areActivitiesEnabled else {
            throw ActivityError.disabled
        }

        let attributes = DeliveryAttributes(
            orderId: orderId,
            restaurantName: restaurantName,
            totalItems: totalItems
        )
        let initialState = DeliveryAttributes.ContentState(
            etaMinutes: 45,
            status: .preparing,
            driverName: "Väntar på förare",
            lastUpdated: Date()
        )
        let content = ActivityContent(
            state: initialState,
            staleDate: Date().addingTimeInterval(60 * 15) // 15 minuter
        )

        activity = try Activity.request(
            attributes: attributes,
            content: content,
            pushType: .token  // Vi vill kunna uppdatera från servern
        )

        // Lyssna på push-token, den KAN roteras under livstiden
        if let activity {
            Task {
                for await tokenData in activity.pushTokenUpdates {
                    let token = tokenData.map { String(format: "%02x", $0) }.joined()
                    await sendTokenToServer(token, orderId: orderId)
                }
            }
        }
    }

    func update(eta: Int, status: DeliveryStatus, driver: String) async {
        guard let activity else { return }
        let updated = DeliveryAttributes.ContentState(
            etaMinutes: eta,
            status: status,
            driverName: driver,
            lastUpdated: Date()
        )
        let content = ActivityContent(
            state: updated,
            staleDate: Date().addingTimeInterval(60 * 10)
        )
        await activity.update(content)
    }

    func end() async {
        guard let activity else { return }
        let final = DeliveryAttributes.ContentState(
            etaMinutes: 0,
            status: .arrived,
            driverName: activity.content.state.driverName,
            lastUpdated: Date()
        )
        await activity.end(
            ActivityContent(state: final, staleDate: nil),
            dismissalPolicy: .after(Date().addingTimeInterval(60 * 5))
        )
    }

    enum ActivityError: Error { case disabled }

    private func sendTokenToServer(_ token: String, orderId: String) async { /* ... */ }
}

dismissalPolicy är värd att stanna vid. .immediate plockar bort aktiviteten direkt. .default låter systemet visa den upp till fyra timmar efter att den avslutats, vilket är bra för leveranser där kvittot är intressant en stund efteråt. .after(date:) ger dig ett tydligt fönster. Jag väljer nästan alltid .after med en förnuftig tid. Standardvärdet fyra timmar är för långt för de flesta appar.

Push-uppdateringar via APNs och broadcast i iOS 26

För långlivade aktiviteter driver du uppdateringarna från servern. När du startar aktiviteten med pushType: .token ber ActivityKit APNs om en token som är unik för just den aktiviteten. Den kan roteras när som helst, och pushTokenUpdates-strömmen ovan hanterar det. Servern skickar sedan en push med typ liveactivity, prioritet 10 för normala uppdateringar eller prioritet 5 för hög frekvens, och en payload som ser ut så här:

{
  "aps": {
    "timestamp": 1752316800,
    "event": "update",
    "content-state": {
      "etaMinutes": 12,
      "status": "onTheWay",
      "driverName": "Anna L.",
      "lastUpdated": "2026-07-13T10:00:00Z"
    },
    "stale-date": 1752320400,
    "relevance-score": 90,
    "alert": {
      "title": "Föraren är på väg",
      "body": "Anna kör mot din adress"
    }
  }
}

iOS 26 lägger till en broadcast-kanal. När du kör en Live Activity som miljontals användare tittar på samtidigt (en fotbollsmatch, ett gate-byte på flygplatsen, en aktiehändelse), kan du starta aktiviteten med pushType: .channel(channelId) istället. Din server skickar då en enda push till kanalen och Apples backend distribuerar den till alla enheter som prenumererar. Serverbelastningen sjunker från "en push per token" till "en push per uppdatering". Detaljerna finns i Apples officiella ActivityKit push-dokumentation.

Två fällor jag har snubblat på. (1) APNs push type liveactivity kräver token-baserad anslutning. p12-certifikat fungerar inte, du måste använda p8. (2) Push-token för aktiviteten är inte samma sak som enhetens push-token. Du kan inte återanvända den befintliga.

Interaktiva knappar med App Intents

Sedan iOS 17 kan Live Activities innehålla knappar och toggles som kör kod utan att öppna appen. Koden är en AppIntent, samma primitiv som driver Siri, Spotlight och Apple Intelligence. Om du har läst min guide till App Intents i iOS 26 kommer det här att kännas bekant: en intent, många ytor.

import AppIntents
import ActivityKit

struct MarkDeliveredIntent: AppIntent {
    static var title: LocalizedStringResource = "Markera som levererad"
    static var isDiscoverable = false  // Inte i Shortcuts

    @Parameter(title: "Order ID") var orderId: String

    init() {}
    init(orderId: String) { self.orderId = orderId }

    func perform() async throws -> some IntentResult {
        // Uppdatera aktiviteten från intent, appen behöver inte öppnas
        for activity in Activity<DeliveryAttributes>.activities
            where activity.attributes.orderId == orderId {
            let final = DeliveryAttributes.ContentState(
                etaMinutes: 0, status: .arrived,
                driverName: activity.content.state.driverName,
                lastUpdated: Date()
            )
            await activity.end(
                ActivityContent(state: final, staleDate: nil),
                dismissalPolicy: .immediate
            )
        }
        return .result()
    }
}

I din expanded-region använder du Button(intent:) så att systemet kopplar knappen till intentet:

DynamicIslandExpandedRegion(.bottom) {
    Button(intent: MarkDeliveredIntent(orderId: context.attributes.orderId)) {
        Label("Levererad", systemImage: "checkmark.circle.fill")
    }
    .tint(.green)
    .accessibilityHint("Markerar ordern som mottagen")
}

Fördelen? Intentet körs i widget-extensionens process, appen behöver inte startas, och samma intent kan vara en Siri-fras och en Apple Intelligence-action. Det är verkligen "en app intent, många platser", en princip som täcks i detalj i WWDC26-sessionen om Live Activities essentials.

Stale-läge, relevans-score och deep linking

När data blir gammal (servern har varit tyst en stund, eller push-budgeten är slut) går aktiviteten in i stale-läge. Systemet ändrar utseendet lite (dämpad opacitet) och sätter context.isStale till true. Bygg gärna en explicit stale-vy:

if context.isStale {
    HStack {
        Image(systemName: "arrow.clockwise")
        Text("Data uppdateras...")
            .foregroundStyle(.secondary)
    }
    .accessibilityLabel("Uppdaterar leveransinformation")
} else {
    LiveDeliveryContent(state: context.state)
}

Relevans-score kommer in när användaren har flera Live Activities aktiva samtidigt. Dynamic Island visar två i taget. Låsskärmen sorterar allt. Sätt relevance-score i payloaden (0 till 100), där högre score åker upp. En taxi som är två minuter bort ska ha högre score än en pizza som är tjugo minuter bort, oavsett vilken som startades först.

Deep linking från Live Activity till relevant vy i appen görs med widgetURL(_:). När användaren trycker på aktiviteten (eller compact-Dynamic Island) öppnar systemet URL:en i din app. Kombinera det med NavigationStack och du kan hoppa direkt till orderdetaljerna. Se min NavigationStack-guide för deep linking för hur du bygger routers som klarar det.

Tillgänglighet och VoiceOver

Jag väver in tillgänglighet från början, för Live Activities låter annars förvirrande i VoiceOver. Snabbt tickande siffror utan sammanhang är brus, inte information. Tre saker att göra i varje projekt:

  • Använd accessibilityLabel på siffervyer så att "12" läses upp som "beräknad ankomst om 12 minuter". Rå-siffror är meningslösa för någon som inte ser skärmen.
  • Gruppera relaterade element med .accessibilityElement(children: .combine) så VoiceOver inte läser upp fem separata delar av samma leveransstatus.
  • Skriv accessibilityHint på interaktiva knappar ("Markerar ordern som mottagen"). Hint förklarar vad som händer, inte vad knappen heter.

Testa med VoiceOver på riktig hårdvara. Aktivera Screen Curtain (VoiceOver plus trippel-tryck med tre fingrar) och navigera Live Activity utan att se den. Det är det snabbaste sättet att upptäcka luckor. För timingkurvor på animerade övergångar: håll dem under 200 ms, för långsammare rörelser missas ofta av användare med reduced motion aktiverat.

Felsökning av throttling och budget

Om din Live Activity plötsligt slutar uppdatera är svaret nästan alltid budget. Systemet har en dold energibudget per aktivitet, och när den tar slut blir alla vidare pushar tysta. Plugga in enheten, öppna Console.app och filtrera på processen liveactivitiesd plus söktermen "Budget". Sedan blir det tydligt:

  • priority(0), budget(0). Systemet har bestämt sig för att inte visa aktiviteten. Använd prioritet 5 och färre pushar.
  • running-not-visible. Aktiviteten kör men renderas inte. Ofta för att stale-tiden är passerad eller ContentState inte kunde avkodas.
  • APNs-anropet returnerar 410 Gone. Token är död. Ta bort den från servern, nästa push med samma token blockas ändå.

Under utveckling: sätt NSSupportsLiveActivitiesFrequentUpdates till YES så att du inte fastnar i throttling under test. I produktion, mät faktisk frekvens. De flesta appar behöver inte den flaggan alls. En leveransapp uppdaterar rimligt sett var femte till tionde minut, inte varje sekund.

Sist: com.apple.developer.live-activities-entitlementet läggs till automatiskt av Xcode när du bockar i capability. Om Certificates, Identifiers & Profiles inte visar Live Activity-alternativet, vilket händer i vissa developer-team, regenerera provisioning-profilen. Utan entitlementet får Activity.request tyst error och du ser ingenting alls. Historiken och nyaste ändringar följer jag alltid i Apples ActivityKit-dokumentation.

Vad du bör bygga härnäst

Ta leverans-skelettet ovan, ersätt DeliveryStatus med din egen domän (timer, träningspass, aktiekurs, matchresultat) och kör igenom hela cykeln på en riktig enhet. Testa särskilt: kallstart efter reboot (aktiviteter överlever inte omstart), Always On-läget på iPhone 14 Pro och senare, samt hur din layout mår i den nya Liquid Glass-materialet. Jag har en separat Liquid Glass-guide för iOS 26 som täcker WidgetRenderingMode och accented mode i detalj. Sedan är du redo att skicka.

Vanliga frågor

Hur länge kan en Live Activity vara aktiv i iOS 26?

En Live Activity kan vara aktiv i upp till 8 timmar och sedan ligga kvar på låsskärmen i stale-läge ytterligare 4 timmar innan systemet tar bort den, totalt 12 timmar. Du kan dock avsluta den när som helst med activity.end och styra dismissalPolicy själv.

Behöver jag Dynamic Island-hårdvara för att bygga Live Activities?

Nej. Dynamic Island kräver iPhone 14 Pro eller senare, men låsskärmspresentationen fungerar på alla iPhones med iOS 16.1 eller nyare. Din ActivityConfiguration måste ändå definiera Dynamic Island-vyer så att appen kör på nyare hårdvara.

Vad är skillnaden mellan token-push och broadcast-push i iOS 26?

Token-push skickas till en enskild aktivitet via en unik push-token, vilket är bra för personlig data som en leverans. Broadcast-push (ny i iOS 26) skickas till en kanal som alla aktivitetsinstanser prenumererar på, perfekt för händelser miljontals användare tittar på samtidigt, som matcher eller gate-ändringar.

Varför uppdateras inte min Live Activity trots att servern skickar pushar?

Nästan alltid budget-throttling. Kontrollera Console.app efter Budget-rader från liveactivitiesd. Åtgärder: sätt NSSupportsLiveActivitiesFrequentUpdates till YES i Info.plist, använd prioritet 5 i APNs-payloaden, och skicka färre pushar. Kontrollera också att din ContentState-payload dekoderar korrekt. Ett stavfel gör att push tas emot men inte renderas.

Kan Live Activities uppdateras utan att appen är öppen?

Ja, det är hela poängen. När du startar aktiviteten med pushType: .token eller .channel kan servern uppdatera den via APNs helt utan att appen körs. Även interaktiva knappar (via App Intents) exekverar i widget-extensionens process, inte i huvudappen.

Ava Thompson
Om Författaren Ava Thompson

SwiftUI engineer focused on declarative animations and accessibility. Will fight you about navigation stacks.