Live Activities i Dynamic Island: Vodič za ActivityKit u iOS 26

Vodič za izradu Live Activities i Dynamic Island u iOS 26 pomoću ActivityKit okvira. Postavljanje Widget ekstenzije, ActivityAttributes, sva tri stanja Dynamic Islanda, App Intents i testiranje u Xcode 26 simulatoru.

Live Activities iOS 26 ActivityKit Vodič

Ažurirano: 7. rujna 2026.

Live Activities su interaktivne obavijesti u realnom vremenu koje iOS 26 aplikacija prikazuje na zaključanom zaslonu, u Dynamic Islandu i (od iOS 26) na Apple Watchu putem Smart Stacka. Dodaju se pomoću ActivityKit okvira, izgled se piše u SwiftUI-u unutar Widget ekstenzije, a stanje se ažurira lokalno ili preko APNs push obavijesti. Ovaj vodič pokazuje kako od nule napraviti Live Activity za praćenje isporuke, dizajnirati sva tri stanja Dynamic Islanda, dodati App Intents interaktivnost i testirati sve u simulatoru.

  • ActivityKit je Appleov okvir za Live Activities; sam izgled se piše u SwiftUI-u unutar Widget ekstenzije, ne u glavnoj aplikaciji.
  • Dynamic Island ima tri stanja (compact leading/trailing, minimal i expanded), i svako se definira zasebno u ActivityConfiguration.
  • ActivityAttributes definiraju statični dio Live Activityja, dok ContentState sadrži dinamične vrijednosti koje se mijenjaju kroz ažuriranja.
  • Live Activity može trajati do 8 sati aktivno i još 4 sata u stanju "ended", nakon čega ga sustav automatski uklanja.
  • iOS 26 donosi ActivityContent.stale API, glađe animacije stanja i podršku za Live Activities na watchOS 26 Smart Stacku.
  • App Intents omogućuju direktne akcije unutar Live Activityja bez otvaranja aplikacije, što je savršeno za "Pauziraj", "Poništi" ili "Označi kao dostavljeno".

Što su Live Activities i kako rade u iOS 26?

Live Activities su posebna vrsta widgeta koja prikazuje podatke u realnom vremenu izvan aplikacije: na zaključanom zaslonu, na StandBy zaslonu, na Dynamic Islandu i, od iOS 26, u watchOS Smart Stacku. Za razliku od običnog widgeta koji se osvježava periodično na temelju vremenske linije, Live Activity ima eksplicitno stanje koje aplikacija ili poslužitelj mijenjaju kad se dogodi konkretan događaj: dostavljač napravi sljedeći korak, gol je zabijen, tajmer dođe do nule.

API se zove ActivityKit i uveden je u iOS 16.1. Radi na iPhoneima s A13 čipom i novijima. U iOS 26 dobio je nekoliko važnih dodataka o kojima ću posebno pisati u dijelovima ispod: novi ActivityContent.stale markers, poboljšani prijelazi za Dynamic Island, mogućnost da se Live Activity reflektira na Apple Watchu bez posebnog watchOS targeta, te bolje ponašanje pod low power modeom. Došao sam iz Objective-C ere gdje smo iste stvari radili remote-notification-content-extension trikovima; ActivityKit je konačno napravio ovo bez hakova.

Životni ciklus je jednostavan. Aplikacija poziva Activity.request(...) da ga pokrene, activity.update(...) da mu promijeni stanje, i activity.end(...) da ga završi. Sustav sam odlučuje gdje ga prikazati (Lock Screen, Dynamic Island, Smart Stack) na temelju istog SwiftUI opisa koji ste dali. Live Activity može trajati do 8 sati u aktivnom stanju i još 4 sata u ended stanju prije nego što ga sustav ukloni. Imajte to na umu kod duljih tokova.

Po čemu se Dynamic Island razlikuje od klasičnog Live Activityja?

Dynamic Island je isti Live Activity, prikazan u drugom prostoru. Ista ActivityConfiguration deklarira i Lock Screen izgled i tri Dynamic Island varijante, što znači jedan izvor istine i jedno mjesto za promjene. Razlika je u tome što Dynamic Island ima strogo ograničen prostor pa vam sustav prosljeđuje jednu od tri regije koje morate ispuniti.

Tri Dynamic Island stanja su:

StanjeKad se pojavljujePreporučeni sadržaj
Compact leading + trailingZadano stanje kad je Live Activity aktivan, a korisnik nije u aplikacijiIkona/glagol lijevo, brojka/vrijeme desno (npr. dostava: ikona kutije lijevo, "12 min" desno)
MinimalKad postoji više aktivnih Live Activityja i vaš nije trenutno prioritetanSamo jedna glyph ili brojka; sve mora stati u ~30×30 pt
ExpandedKad korisnik dodirne i drži Dynamic IslandPuni prikaz s ikonom, progresom, akcijama; do 4 regije: leading, trailing, center, bottom

U iOS 26 Apple je poravnao animacije prijelaza između ova tri stanja. matchedGeometryEffect i implicitne .animation modifikatore ActivityKit sada honorira dosljedno između minimal → compact → expanded prijelaza, što ranije nije uvijek bio slučaj. Ako ste pisali nekakve DispatchQueue.main.asyncAfter workarounde za sinkronizaciju animacije, sad ih možete izbaciti. (Iskreno, jedva čekam da to obrišem iz svog starog koda.)

Postavljanje Widget ekstenzije za ActivityKit

Live Activity ne živi u glavnoj aplikaciji. Živi u Widget ekstenziji, zajedno s ostalim widgetima. Ako ekstenziju već imate, dodajte samo ActivityConfiguration. Ako nemate, dodajte novi target: File → New → Target → Widget Extension, uključite "Include Live Activity" checkbox, i Xcode 26 vam generira boilerplate.

Nakon toga otvorite Info.plist glavne aplikacije (ne ekstenzije!) i dodajte:

<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>

Prvi ključ omogućuje Live Activities. Drugi je opcionalan; dodajte ga samo ako morate ažurirati češće od otprilike jednog ažuriranja u minuti. Bez njega sustav agresivno throttla push updates u pozadini. Zajednički kod (ActivityAttributes tip) stavite u zajednički Swift file koji je član oba targeta, ili u zaseban framework. Inače će Xcode prijaviti "type not found" kad pokušate pokrenuti Activity iz glavne aplikacije.

Definiranje ActivityAttributes i ContentState

Svaki Live Activity ima dva sloja podataka: statični dio (koji se ne mijenja tijekom života aktiviteta) i dinamični dio (ContentState, koji se mijenja kroz ažuriranja). Oba spremate u tip koji implementira ActivityAttributes protokol.

import ActivityKit
import Foundation

struct DeliveryAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable {
        var status: DeliveryStatus
        var etaMinutes: Int
        var driverName: String
        var stepsCompleted: Int
    }

    // Statični dio: postavlja se jednom, pri pokretanju
    let orderNumber: String
    let restaurantName: String
    let totalSteps: Int
}

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

Tri stvari koje su me zezale prvi put. ContentState mora biti Codable i Hashable (ne Equatable, jer ga Hashable podrazumijeva). Sve što stavljate u state mora se serijalizirati u ~4 KB JSON payload (limit APNs push update-a). I enumeriranja moraju imati raw values ili custom Codable, jer im ContentState Diffing algoritam u iOS 26 to očekuje. Ovaj zadnji sam otkrio na najgori mogući način, u produkciji.

SwiftUI izgled za Lock Screen i Dynamic Island

Sav izgled deklarirate u jednom Widget tipu unutar Widget ekstenzije. ActivityConfiguration traži dvije stvari: closure za Lock Screen (i StandBy) prikaz, i dynamicIsland closure koji vraća sva tri stanja.

import ActivityKit
import SwiftUI
import WidgetKit

struct DeliveryLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DeliveryAttributes.self) { context in
            // Lock Screen / StandBy prikaz
            LockScreenView(context: context)
                .activityBackgroundTint(.black.opacity(0.85))
                .activitySystemActionForegroundColor(.white)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    Label(context.attributes.restaurantName, systemImage: "bag.fill")
                        .font(.caption)
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("\(context.state.etaMinutes) min")
                        .font(.headline)
                        .monospacedDigit()
                }
                DynamicIslandExpandedRegion(.bottom) {
                    ProgressView(
                        value: Double(context.state.stepsCompleted),
                        total: Double(context.attributes.totalSteps)
                    )
                    .tint(.green)
                }
            } compactLeading: {
                Image(systemName: "bag.fill")
                    .foregroundStyle(.green)
            } compactTrailing: {
                Text("\(context.state.etaMinutes)m")
                    .monospacedDigit()
            } minimal: {
                Image(systemName: "bag.fill")
                    .foregroundStyle(.green)
            }
        }
    }
}

Nekoliko stvari na koje pazim u svakom Live Activity projektu. Prvo, monospacedDigit() na brojkama koje se često mijenjaju (etaMinutes, kola sata) da se ne trza layout kad "1" postane "10". Drugo, context.state za dinamične vrijednosti i context.attributes za statične. Treće, držanje compact i minimal stanja vrlo jednostavnima jer sustav često skalira Dynamic Island, a tanke glyph ilustracije se najbolje ponašaju. Za dizajn koji koristi transition efekte pogledajte SwiftUI SensoryFeedback za haptički odziv u iOS 26; kombinacija vibro-signala i vizualnog prijelaza podiže percepciju kvalitete znatno više nego što bi trebala.

Pokretanje, ažuriranje i završavanje Live Activityja

Iz glavne aplikacije poziva se Activity.request(...). Vraća Activity objekt koji držite negdje (npr. u @Observable servisu) da ga kasnije možete ažurirati. U iOS 26 API je konzistentno async, pa zaboravite ranije completion handlere.

import ActivityKit

@Observable
final class DeliveryTracker {
    private var activity: Activity<DeliveryAttributes>?

    func start(orderNumber: String, restaurant: String, driver: String) async {
        let attributes = DeliveryAttributes(
            orderNumber: orderNumber,
            restaurantName: restaurant,
            totalSteps: 4
        )
        let initial = DeliveryAttributes.ContentState(
            status: .preparing,
            etaMinutes: 25,
            driverName: driver,
            stepsCompleted: 1
        )
        let content = ActivityContent(state: initial, staleDate: .now.addingTimeInterval(60 * 30))

        do {
            activity = try Activity.request(
                attributes: attributes,
                content: content,
                pushType: .token
            )
        } catch {
            print("Live Activity nije mogao startati: \(error)")
        }
    }

    func advance(to status: DeliveryStatus, etaMinutes: Int, step: Int) async {
        guard let activity else { return }
        let newState = DeliveryAttributes.ContentState(
            status: status,
            etaMinutes: etaMinutes,
            driverName: activity.content.state.driverName,
            stepsCompleted: step
        )
        await activity.update(
            ActivityContent(state: newState, staleDate: .now.addingTimeInterval(60 * 10))
        )
    }

    func finish() async {
        guard let activity else { return }
        let finalState = DeliveryAttributes.ContentState(
            status: .arrived,
            etaMinutes: 0,
            driverName: activity.content.state.driverName,
            stepsCompleted: activity.attributes.totalSteps
        )
        await activity.end(
            ActivityContent(state: finalState, staleDate: nil),
            dismissalPolicy: .after(.now.addingTimeInterval(60 * 5))
        )
    }
}

Par sitnica koje me nauče projekti u produkciji. staleDate obavezno postavljajte; nakon njega sustav prikazuje activity zasivljen s natpisom "Zastarjelo" i to je bolje UX nego lažiranje svježih vrijednosti. Kod end(...) koristite .after(...) policy da korisnik vidi rezultat nekoliko minuta prije nego što nestane. .immediate ubija activity odmah, što najčešće nije ono što želite. Za navigaciju iz Live Activityja u aplikaciju iskoristite deep linking pattern iz vodiča za SwiftUI NavigationStack u iOS 26.

Ažuriranje putem APNs push obavijesti

Za većinu produkcijskih tokova ažurirat ćete Live Activity s poslužitelja, ne iz aplikacije, jer aplikacija u pozadini najčešće ne radi kad se stanje mijenja. ActivityKit ima svoj push token za to, koji je odvojen od klasičnog device tokena.

// Nakon što je Activity kreiran s pushType: .token,
// pretplatimo se na push token za taj konkretan Activity
Task {
    guard let activity else { return }
    for await tokenData in activity.pushTokenUpdates {
        let token = tokenData.map { String(format: "%02x", $0) }.joined()
        // Pošaljite ovaj token vašem backendu, vezan uz orderNumber
        await BackendClient.shared.register(pushToken: token, for: activity.attributes.orderNumber)
    }
}

Payload koji šaljete s poslužitelja ima apns-push-type: liveactivity header, apns-topic: com.tvrtka.aplikacija.push-type.liveactivity, i JSON tijelo s aps.timestamp, aps.event (update ili end) i aps.content-state koji mora točno odgovarati vašem ContentState tipu. Detaljnu specifikaciju formata pogledajte u Apple dokumentaciji za ActivityKit push obavijesti. Za low-latency scenarije (sport, dionice) u iOS 26 postoji i apns-priority: 10 u kombinaciji s NSSupportsLiveActivitiesFrequentUpdates. Ali oprez: sustav mjeri prosjek i previše frequent updateova može uzrokovati da vaš activity bude throttled do kraja svog života. Ovaj scenarij sam vidio uživo dok sam optimizirao live-score aplikaciju, i vjerujte mi, korisnici se ne vraćaju.

Interaktivnost pomoću App Intents

Od iOS 17 Live Activity može imati gumbe koji direktno pokreću akciju bez otvaranja aplikacije. Realizirano je kroz App Intents. Definirate AppIntent, umećete Button(intent:) u ekspanded regiju, i sustav pokreće intent u pozadini glavne aplikacije.

import AppIntents
import ActivityKit

struct MarkDeliveredIntent: LiveActivityIntent {
    static var title: LocalizedStringResource = "Označi kao dostavljeno"

    @Parameter(title: "ID narudžbe")
    var orderNumber: String

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

    func perform() async throws -> some IntentResult {
        await DeliveryTracker.shared.markDelivered(orderNumber: orderNumber)
        return .result()
    }
}

U ekspanded regiji dinamičkog otoka onda:

DynamicIslandExpandedRegion(.bottom) {
    Button(intent: MarkDeliveredIntent(orderNumber: context.attributes.orderNumber)) {
        Label("Dostavljeno", systemImage: "checkmark.circle.fill")
    }
    .tint(.green)
    .buttonStyle(.borderedProminent)
}

LiveActivityIntent (a ne obični AppIntent) je bitan. Samo taj protokol garantira da perform() radi u kontekstu glavne aplikacije i ima direktan pristup share containerima. Ako ste već implementirali App Intents za Siri i Shortcuts, možete ih ponovno iskoristiti; dubinski pregled kako pisati intents nalazi se u vodiču App Intents u iOS 26 za Siri, Shortcuts i Apple Intelligence. Za kompletnu referencu novih iOS 26 API-ja vezanih uz Live Activities pogledajte i službenu ActivityKit dokumentaciju.

Kako testirati Live Activity u Xcode 26 simulatoru?

Xcode 26 simulator konačno podržava Dynamic Island. Na iPhone 15 Pro simulatoru i novijima pojavljuje se pravi otok koji reagira na pritisak i dugi pritisak. Za testiranje pokrenite aplikaciju, izazovite Activity.request(...) iz nekog debug gumba, pa pritisnite Cmd+L da simulirate zaključavanje i vidite Lock Screen prikaz.

Za push updateove postoji sjajan trik. Xcode 26 čita .apns JSON datoteke iz projektnog navigatora; kliknete desnim tipkom miša na datoteku pa Send Push Notification to iOS Device. To radi i za Live Activity ako u JSON dodate Simulator Target Bundle polje s bundle ID-em vaše widget ekstenzije i Simulator Live Activity ID s ID-em aktivnog activityja (dobijete ga iz activity.id). Šablonu i punu proceduru za push simulaciju pronaći ćete u Appleovom vodiču za testiranje Live Activities.

Često postavljana pitanja

Koliko dugo Live Activity može biti aktivan?

Aktivan Live Activity može trajati do 8 sati. Nakon toga sustav ga automatski završi. Ako pozovete end(...) ranije, activity može ostati u "ended" stanju još do 4 sata prije nego što potpuno nestane s Lock Screena, što je korisno za prikaz konačnog stanja korisniku.

Trebam li push notification permission za Live Activities?

Ne. Live Activities koriste odvojen sustav autorizacije koji je zadano uključen. Korisnik ih može isključiti u Settings → Face ID & Passcode → Live Activities ili za pojedinu aplikaciju u Settings → [Aplikacija] → Live Activities. Za push updateove potreban je posebni Live Activity push token, ne klasični device token.

Koja je razlika između Widget i Live Activity?

Widget se osvježava periodično prema TimelineProvideru koji vraća unaprijed pripremljenu vremensku liniju. Live Activity ima eksplicitno stanje koje mijenja aplikacija ili poslužitelj kroz update(...) pozive i vidljiv je izvan Home Screena, na Lock Screenu i Dynamic Islandu. Live Activity se pojavljuje samo dok je događaj u tijeku.

Zašto se moj Live Activity ne pojavljuje u Dynamic Islandu?

Najčešći razlozi su tri: uređaj nema Dynamic Island (samo iPhone 14 Pro i noviji), nedostaje NSSupportsLiveActivities u Info.plist-u glavne aplikacije, ili je istovremeno aktivno više Live Activityja pa je vaš u minimal stanju. Provjerite i da ActivityConfiguration stvarno vraća DynamicIsland; bez toga, Lock Screen radi, a Island ne.

Podržavaju li Live Activities Apple Watch u iOS 26?

Da. iOS 26 automatski reflektira Live Activity u watchOS 26 Smart Stack koristeći isti SwiftUI izgled iz kompaktnih Dynamic Island regija, bez potrebe za posebnim watchOS targetom. Ako vam treba prilagođen watchOS izgled, dodajte supplementalActivityFamilies([.small]) modifikator i vratite WatchLayoutView u odgovarajućem branchu.

Lukas Müller
O Autoru Lukas Müller

iOS developer and Swift author since the Objective-C days. Spends his evenings on side projects and his mornings on SwiftUI internals.