Live Activities ja Dynamic Island ovat muodostuneet aika lailla keskeiseksi osaksi modernia iOS-käyttökokemusta — eikä syyttä. iOS 26:n myötä ActivityKit-kehys on saanut entistä laajemman alustatuen: toiminnot näkyvät nyt iPhonen lukitusnäytöllä, Dynamic Islandissa, paritetussa Apple Watchissa, CarPlayssa ja jopa macOS 26:ssa (kyllä, valikkopalkissa). Tässä oppaassa käymme alusta loppuun läpi sen, miten rakennat tuotantotason Live Activityn SwiftUI:lla, ActivityKitillä ja APNs-pohjaisilla push-päivityksillä. Selkokielisesti ja ilman turhaa pumpulia.
Live Activities ja Dynamic Island iOS 26:ssa: Käytännön opas ActivityKit-kehitykseen
Opi rakentamaan Live Activities -toimintoja ja Dynamic Island -käyttöliittymiä iOS 26:lle ActivityKitin ja SwiftUI:n avulla. Sisältää APNs-integraation, koodiesimerkit ja parhaat käytännöt vuodelle 2026.

Live Activity on iOS-käyttöliittymäelementti, joka pysyy kiinnitettynä lukitusnäytölle ja Dynamic Islandiin niin kauan kuin meneillään oleva tapahtuma jatkuu. Esimerkkejä? Taksimatka, ruokalähetys, urheilupeli, ajastin, kuntoiluharjoitus — sellaisia juttuja, jotka loppuvat jossain vaiheessa. Päivitykset tulevat joko suoraan sovelluksesta tai palvelimelta APNs:n kautta.
Toisin kuin widget — joka on pysyvä ja päivittyy järjestelmän aikataulun mukaan — Live Activity on olemassa vain tapahtuman ajan. Yksittäinen aktiviteetti voi olla aktiivinen enintään 8 tuntia, ja se pysyy lukitusnäytöllä vielä 4 tuntia "vanhentuneessa" tilassa. Yhteensä siis enintään 12 tuntia, jonka jälkeen järjestelmä hylkää sen. Ei pidempään, vaikka kuinka yrittäisit.
Tyypilliset käyttötapaukset 2026
- Toimitus- ja kuljetussovellukset: reaaliaikainen seuranta saapumiseen asti.
- Urheilu ja eSports: elävä tulospalvelu ottelun aikana.
- Ajastimet ja kuntoilu: käynnissä olevan harjoituksen tai pomodoron näyttäminen.
- Taloustyökalut: osakekurssin tai kryptohinnan reaaliaikainen seuranta (omasta kokemuksesta — älä tee tästä jatkuvaa, akku kärsii).
- Matkustaminen: portin vaihto ja lentoyhteyden tila.
Mikä on uutta iOS 26:ssa?
iOS 26 ei tuonut radikaaleja rikkovia muutoksia ActivityKitin ydinrajapintaan, mutta alustan kattavuus laajeni — ja kunnolla:
- CarPlay-tuki: Live Activities näkyy autossa kuljettajalle.
- macOS Tahoe (26): iPhonen aktiviteetit peilautuvat Macin valikkopalkkiin.
- Liquid Glass -ulkoasu: uusi materiaalivaikutus on käytettävissä myös Live Activity -näkymissä
GlassEffect-modifioijan kautta. - watchOS 26 Smart Stack: Live Activities tulevat näkyviin älypinoon kontekstin perusteella.
- Push-to-Start (iOS 17.2+): serveri voi käynnistää Live Activityn kokonaan ilman, että sovellus on auki.
- visionOS 26: yhteensopiva iPhone-/iPad-sovelluksen Live Activities tulee saataville myös visionOS:lle.
Rehellisesti sanottuna CarPlay-tuki on se, mitä monet kehittäjät ovat odottaneet pisimpään. Ruokalähetys-sovelluksilla se on lähes pakollinen ominaisuus tänä päivänä.
Projektin alustus Xcode 26:ssa
Live Activity tarvitsee Widget Extension -kohteen. Lisää se valitsemalla File → New → Target → Widget Extension. Älä rastita "Include Configuration Intent" -valintaa, ellet erikseen tarvitse sitä — siitä tulee vain ylimääräistä sotkua.
Vaadittavat asetukset
- Avaa sovelluksen
Info.plistja lisää avainNSSupportsLiveActivitiesarvollaYES. - Halutessasi lisää
NSSupportsLiveActivitiesFrequentUpdatespäästäksesi tiheämpiin paikallisiin päivityksiin. - Lisää Push Notifications -ominaisuus pääsovellustargettiin (Signing & Capabilities -välilehti), jos käytät palvelinpäivityksiä.
- Vähimmäisversio: iOS 16.1 lukitusnäytölle, iOS 16.2 Dynamic Islandille, iOS 17.2 Push-to-Start-toiminnolle.
ActivityAttributes: staattisen ja dynaamisen datan erottelu
ActivityAttributes-protokolla määrittää datamallin Live Activitylle. Keskeinen periaate on yksinkertainen: erottele muuttumaton metadata (asetetaan vain käynnistyksen yhteydessä) ja elävä sisältötila (ContentState), joka muuttuu tapahtuman edetessä. Sekoittaminen näiden välillä on yksi yleisimmistä alkuvaiheen virheistä.
import ActivityKit
import Foundation
struct DeliveryAttributes: ActivityAttributes {
// Dynaaminen sisältö — päivittyy ajan mittaan
public struct ContentState: Codable, Hashable {
var status: DeliveryStatus
var etaMinutes: Int
var driverName: String?
var progress: Double // 0.0...1.0
}
// Staattinen metadata — asetetaan kerran
var orderId: String
var restaurantName: String
var deliveryAddress: String
}
enum DeliveryStatus: String, Codable, Hashable {
case preparing
case onTheWay
case arrived
}
Tärkeä raja, johon kannattaa varautua: jokaisen ContentState-päivityksen koodattu JSON-paketti saa olla enintään 4 kt. Pidä siis vain todella tarvittava tieto dynaamisessa tilassa. Ei mitään ylimääräistä metaa.
Live Activity -näkymän rakentaminen SwiftUI:lla
Käyttöliittymä määritellään ActivityConfiguration-rakenteella. Se sisältää kaksi sulkeumaa: lukitusnäytön ulkoasu ja Dynamic Island -ulkoasu eri kokoisina (laajennettu, kompakti, minimaalinen).
import ActivityKit
import SwiftUI
import WidgetKit
struct DeliveryLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lukitusnäytön ulkoasu
LockScreenDeliveryView(
state: context.state,
attributes: context.attributes
)
.activityBackgroundTint(.black.opacity(0.6))
.activitySystemActionForegroundColor(.white)
} dynamicIsland: { context in
DynamicIsland {
// Laajennettu tila (pitkä painallus tai automaattinen)
DynamicIslandExpandedRegion(.leading) {
Image(systemName: iconName(for: context.state.status))
.font(.title2)
.foregroundStyle(.tint)
}
DynamicIslandExpandedRegion(.trailing) {
Text("\(context.state.etaMinutes) min")
.font(.headline.monospacedDigit())
}
DynamicIslandExpandedRegion(.bottom) {
ProgressView(value: context.state.progress)
.tint(.green)
Text(context.attributes.restaurantName)
.font(.caption)
}
} compactLeading: {
Image(systemName: "bag.fill")
} compactTrailing: {
Text("\(context.state.etaMinutes)m")
.monospacedDigit()
} minimal: {
Image(systemName: "bag.fill")
}
.keylineTint(.green)
.widgetURL(URL(string: "myapp://order/\(context.attributes.orderId)"))
}
}
private func iconName(for status: DeliveryStatus) -> String {
switch status {
case .preparing: return "frying.pan"
case .onTheWay: return "bicycle"
case .arrived: return "house.fill"
}
}
}
Lukitusnäytön näkymä Liquid Glassilla
struct LockScreenDeliveryView: View {
let state: DeliveryAttributes.ContentState
let attributes: DeliveryAttributes
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(attributes.restaurantName)
.font(.headline)
Spacer()
Text("\(state.etaMinutes) min")
.monospacedDigit()
.foregroundStyle(.green)
}
ProgressView(value: state.progress)
.tint(.green)
if let driver = state.driverName {
Text("Kuljettaja: \(driver)")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding()
.glassEffect(.regular, in: .rect(cornerRadius: 16)) // iOS 26
}
}
Live Activityn käynnistäminen sovelluksesta
Käynnistä aktiviteetti silloin, kun varsinainen tapahtuma alkaa — esimerkiksi heti kun käyttäjä on maksanut tilauksen. Tarkista ensin, onko käyttäjä antanut luvan. Jos et tarkista, törmäät ennen pitkää virheeseen.
import ActivityKit
@MainActor
final class DeliveryActivityManager {
private(set) var current: Activity<DeliveryAttributes>?
func start(orderId: String, restaurant: String, address: String) async throws {
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
throw ActivityError.notAuthorized
}
let attributes = DeliveryAttributes(
orderId: orderId,
restaurantName: restaurant,
deliveryAddress: address
)
let initialState = DeliveryAttributes.ContentState(
status: .preparing,
etaMinutes: 35,
driverName: nil,
progress: 0.05
)
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil),
pushType: .token // pyydetään APNs-token palvelinpäivityksiä varten
)
self.current = activity
// Tilaa push-tokenin päivitykset taustasäikeenä
Task { await observePushTokens(for: activity) }
}
private func observePushTokens(for activity: Activity<DeliveryAttributes>) async {
for await tokenData in activity.pushTokenUpdates {
let token = tokenData.map { String(format: "%02x", $0) }.joined()
await sendTokenToServer(token, orderId: activity.attributes.orderId)
}
}
private func sendTokenToServer(_ token: String, orderId: String) async {
// POST sovelluksesi backendille
}
enum ActivityError: Error { case notAuthorized }
}
Päivittäminen ja päättäminen sovelluksesta
extension DeliveryActivityManager {
func update(status: DeliveryStatus, etaMinutes: Int, progress: Double, driver: String?) async {
guard let activity = current else { return }
let newState = DeliveryAttributes.ContentState(
status: status,
etaMinutes: etaMinutes,
driverName: driver,
progress: progress
)
await activity.update(.init(
state: newState,
staleDate: Date().addingTimeInterval(60 * 10) // vanhenee 10 min kuluttua
))
}
func end() async {
guard let activity = current else { return }
let finalState = DeliveryAttributes.ContentState(
status: .arrived,
etaMinutes: 0,
driverName: activity.content.state.driverName,
progress: 1.0
)
await activity.end(
.init(state: finalState, staleDate: nil),
dismissalPolicy: .after(Date().addingTimeInterval(60 * 5))
)
current = nil
}
}
APNs-päivitykset palvelimelta
Kun Live Activity käynnistetään pushType: .token -parametrillä, ActivityKit pyytää APNs:lta yksilöllisen push-tokenin. Tämä token eroaa tavallisesta laitetokenista — se on uniikki kullekin Live Activitylle. Älä siis sekoita näitä keskenään palvelinkoodissasi.
APNs-paketin rakenne
{
"aps": {
"timestamp": 1746883200,
"event": "update",
"content-state": {
"status": "onTheWay",
"etaMinutes": 12,
"driverName": "Mikko",
"progress": 0.62
},
"stale-date": 1746884400,
"alert": {
"title": "Tilaus matkalla",
"body": "Mikko saapuu noin 12 minuutissa"
}
}
}
Muutama keskeinen yksityiskohta, jotka kannattaa pitää mielessä:
- Käytä HTTP/2-päätettä
apns-push-type: liveactivity. apns-topic-otsakkeen pitää ollacom.yritys.sovellus.push-type.liveactivity.- Vain .p8-token-pohjainen autentikointi — Apple ei tue p12-sertifikaatteja Live Activityille (tämä on yllättänyt monta vanhempaa projektia).
- JWT-token on virkistettävä vähintään kerran tunnissa, mutta ei useammin kuin kerran 20 minuutissa.
- Aseta
event-arvoksiupdatepäivityksille taiendpäättämiselle. - Aseta
timestampsekunteina vuoden 1970 alusta — järjestelmä käyttää sitä järjestyksen takaamiseen.
Push-to-Start (iOS 17.2+)
Voit käynnistää Live Activityn ilman, että sovellus on auki. Aika hieno ominaisuus, kun sitä oppii käyttämään. Tilaa ensin push-to-start-tokeneita sovelluksen elinkaaren alussa:
Task {
for await tokenData in Activity<DeliveryAttributes>.pushToStartTokenUpdates {
let token = tokenData.map { String(format: "%02x", $0) }.joined()
await sendStartTokenToServer(token)
}
}
Palvelin lähettää APNs-paketin, jonka event-arvo on start ja attributes-type sisältää ActivityAttributes-tyypin nimen. Siinä se, periaatteessa.
Interaktiivisuus: napit ja App Intents
iOS 17:stä alkaen Live Activity ja Dynamic Island tukevat interaktiivisia kontrolleja App Intents -kehyksen kautta. iOS 26:ssa tämä yhdistyy entistä saumattomammin laajaan Apple Intelligence -ekosysteemiin — mikä avaa ihan uusia mahdollisuuksia esim. ehdotuksille.
import AppIntents
struct CallDriverIntent: AppIntent {
static var title: LocalizedStringResource = "Soita kuljettajalle"
@Parameter(title: "Tilaus")
var orderId: String
func perform() async throws -> some IntentResult {
// Avaa puhelinsovellus tai sisäinen VoIP
return .result()
}
}
// Live Activity -näkymässä:
Button(intent: CallDriverIntent(orderId: context.attributes.orderId)) {
Image(systemName: "phone.fill")
}
.tint(.green)
Esikatselu Xcode Canvasilla
#Preview-makrolla voit esikatsella Live Activitya kaikissa eri tiloissa ilman, että sinun tarvitsee käynnistää sovellusta uudelleen. Tämä säästää uskomattoman paljon aikaa.
#Preview("Compact", as: .dynamicIsland(.compact),
using: DeliveryAttributes(orderId: "A123", restaurantName: "Pizzeria",
deliveryAddress: "Mannerheimintie 1")) {
DeliveryLiveActivity()
} contentStates: {
DeliveryAttributes.ContentState(status: .preparing, etaMinutes: 30,
driverName: nil, progress: 0.1)
DeliveryAttributes.ContentState(status: .onTheWay, etaMinutes: 12,
driverName: "Mikko", progress: 0.6)
DeliveryAttributes.ContentState(status: .arrived, etaMinutes: 0,
driverName: "Mikko", progress: 1.0)
}
Parhaat käytännöt 2026
- Pidä ContentState pienenä: alle 4 kt rajan reilusti — ihanteellisesti alle 1 kt.
- Käytä monospaced numeroita ETA-/aikalukemiin, jotta UI ei hyppäile (pieni juttu, mutta huomataan heti kun se puuttuu).
- Aseta aina
staleDate: näin järjestelmä osaa näyttää käyttäjälle, että data on vanhentunut. - Käytä
.activitySystemActionForegroundColortaataksesi luettavuuden taustasta riippumatta. - Älä päivitä liian usein: tarpeeton liikenne syö akkua ja saa järjestelmän rajoittamaan päivityksiä.
- Vältä animaatioita Dynamic Island -näkymissä — ne on suunniteltu staattisiksi tilakuviksi.
- Testaa CarPlayssa ja Apple Watchilla: ulkoasusi voi näyttää aivan erilaiselta paritetulla laitteella.
- Käytä Liquid Glass -modifioijia (
.glassEffect) iOS 26:ssa yhtenäisen ilmeen takaamiseksi. - Tarjoa fallback-päivitys paikallisesti jos APNs-paketti epäonnistuu ja sovellus pääsee taustaan.
Yleisiä virheitä ja niiden korjaaminen
- "Activities are not enabled" — käyttäjä on poistanut Live Activities -toiminnon Asetuksista. Tarkista
ActivityAuthorizationInfo().areActivitiesEnabledennen yritystä. - Push-päivitys ei toteudu — varmista, että
apns-push-typeonliveactivityjaapns-topicpäättyy.push-type.liveactivity. Tämä on klassinen kompastuskivi. - JSON ei dekoodaudu — käytä oletusarvoista
JSONEncoder-strategiaa palvelimella, älä mukautettuja avainnimiä. - UI ei päivity — varmista, että muokkaat
ContentState-rakennetta etkä staattisiaActivityAttributes-kenttiä. - 4 kt ylitys — pakkaa tila tiiviimmin, käytä lyhyitä avaimia tai siirrä harvoin muuttuva data staattiseen
attributes-osaan.
Usein kysytyt kysymykset
Mikä on ero Live Activityn ja widgetin välillä?
Widget on pysyvä komponentti, joka päivittyy järjestelmän ajastusten mukaan ja näyttää tietoa pidempään. Live Activity puolestaan elää vain meneillään olevan tapahtuman ajan (max. 12 tuntia) ja näkyy lukitusnäytöllä sekä Dynamic Islandissa. Live Activity tukee push-päivityksiä, joiden ansiosta tieto pysyy tuoreena ilman sovelluksen avaamista.
Voinko käyttää samaa widget-extensionia molempiin?
Kyllä voi. Sama Widget Extension -targetti voi sisältää sekä tavallisia Widget-määrityksiä että yhden tai useamman ActivityConfiguration-määrityksen. Molemmat jakavat saman SwiftUI-renderöintiympäristön ja samat rajoitukset (ei animaatioita, rajoitettu API-pinta).
Tarvitsenko palvelimen lähettääkseni Live Activity -päivityksiä?
Et välttämättä. Voit päivittää aktiviteettia paikallisesti niin kauan kuin sovellus on auki tai sillä on taustaruntime. Mutta jos haluat luotettavia päivityksiä silloinkin, kun sovellus on suljettu, tarvitset APNs-pohjaisen palvelimen ja pushType: .token -käynnistyksen. Käytännössä useimmat tuotantosovellukset tekevät niin.
Toimivatko Live Activities iPadilla tai macOS:ssä?
iOS 26:ssa ja macOS 26:ssa iPhonella käynnistetyt aktiviteetit peilautuvat oletuksena paritetuille Apple Watch- ja Mac-laitteille. iPad ei vielä tarjoa omaa lukitusnäytön Live Activitya (ehkä joskus tulevaisuudessa), mutta visionOS 26 ja CarPlay tukevat niitä natiivisti.
Kuinka kauan Live Activity voi olla aktiivinen?
Yksittäinen aktiviteetti voi olla aktiivinen enintään 8 tuntia ja näkyä lukitusnäytöllä vielä 4 tuntia "vanhentuneena", yhteensä siis enintään 12 tuntia. Sen jälkeen järjestelmä hylkää sen automaattisesti, ja sinun on käynnistettävä uusi aktiviteetti tarvittaessa.
Tarvitseeko Live Activity erillisen App Store -tarkistuksen?
Ei. Live Activities on tavallinen iOS-ominaisuus, joka kulkee normaalin App Store -arvioinnin mukana. Varmista kuitenkin, että käyttötapauksesi vastaa Applen ohjeita: aktiviteetin pitää liittyä todelliseen, käyttäjän aloittamaan tapahtumaan — ei mainoksiin tai jatkuviin notifikaatioihin. Tästä on tullut hylkäyksiä.
Yhteenveto
Live Activities ja Dynamic Island ovat siirtyneet "kivasta lisäominaisuudesta" pakolliseksi osaksi laadukasta iOS-sovellusta vuonna 2026. iOS 26:n laajentunut alustakatto, Liquid Glass -ulkoasu ja Push-to-Start-toiminto tekevät niistä entistä tehokkaampia.
Aloita yksinkertaisella ActivityAttributes-mallilla, rakenna SwiftUI-näkymät kaikille Dynamic Island -tiloille, integroi APNs-token-pohjainen palvelin, ja muista pitää ContentState tiiviinä. Näiden perusteiden hallinnan jälkeen voit syventyä App Intents -interaktiivisuuteen ja tehdä Live Activitysta sovelluksesi tärkeimmän etusivun. Kannattaa kokeilla — käyttäjät huomaavat eron heti.


