Live Activities a Dynamic Island v iOS 26: Kompletný sprievodca ActivityKit, StandBy a watchOS Smart Stack
Naučte sa stavať Live Activities a Dynamic Island v iOS 26 cez ActivityKit, App Intents a APNs. Praktické ukážky kódu pre lock screen, StandBy a Smart Stack na watchOS 26.
Live Activities a Dynamic Island v iOS 26 sú postavené nad frameworkom ActivityKit, ktorý umožňuje aplikácii zobraziť živý, priebežne aktualizovaný stav úlohy mimo aplikácie: na uzamknutej obrazovke, v Dynamic Island, v režime StandBy a na Apple Watch v Smart Stacku. V iOS 26 Apple rozšírilo budget pre aktualizácie, pridalo natívnu integráciu s App Intents pre interaktívne tlačidlá a po prvýkrát doviedlo Live Activities priamo do widget galérie watchOS bez nutnosti samostatného watchOS targetu. V tomto sprievodcovi prejdeme od prvého ActivityAttributes až po push aktualizácie cez APNs a typické chyby, na ktoré som narazil v produkcii.
Live Activity je definovaná dvojicou typov: ActivityAttributes (statický kontext) a vnorený ContentState (premenlivý stav). Obe musia byť Codable a Hashable.
Dynamic Island vyžaduje tri samostatné prezentácie (compact, minimal a expanded). iOS si vyberá podľa kontextu a počtu súbežných aktivít.
V iOS 26 sú lokálne aktualizácie cez activity.update(_:) bez limitu počas behu aplikácie v popredí; v pozadí platí budget okolo 4 až 6 aktualizácií za hodinu, zvyšok musí ísť cez APNs.
Interaktívne tlačidlá v Dynamic Island fungujú výhradne cez AppIntent. Bežné SwiftUI akcie sa v procese widgetu nespúšťajú.
StandBy v iOS 26 zobrazuje Live Activities cez celú obrazovku iPhonu na nabíjačke; treba dodať .contentMargins(.standBy, ...), aby obsah neorezalo.
Na watchOS 26 sa Live Activities zobrazia v Smart Stacku automaticky, ak je iPhone aplikácia podpísaná. Samostatný watchOS target už nie je nutný.
Čo sú Live Activities a ako súvisia s Dynamic Island
Live Activity je perzistentný UI prvok, ktorý beží v samostatnom widget procese, nie v procese vašej aplikácie. Práve preto musí byť celé UI napísané v SwiftUI s obmedzeniami WidgetKitu (žiadne ScrollView, žiadne interaktívne TextField, žiadne sieťovanie z view). Dynamic Island je hardvérová oblasť okolo TrueDepth kamery na iPhone 14 Pro a novších, ktorá dokáže zobraziť kompaktnú, minimálnu alebo rozšírenú prezentáciu tej istej aktivity. Na zariadeniach bez Dynamic Island (iPhone SE, iPad) sa Live Activity prejaví len na uzamknutej obrazovke a v notifikačnom centre.
Honestly, najčastejšie nedorozumenie, na ktoré v code review narážam, je predstava, že Dynamic Island a Live Activity sú dve oddelené funkcie. Nie sú. Dynamic Island je len jedno z miest, kde sa Live Activity vykresluje. Tá istá ActivityConfiguration definuje aj lock screen layout aj všetky tri Dynamic Island regióny. iOS rozhoduje, ktorú prezentáciu zobrazí, podľa toho, či je iPhone uzamknutý, koľko súbežných aktivít beží a či používateľ Dynamic Island podrží. Ak píšete pre nový dizajn Liquid Glass v iOS 26, pamätajte, že pozadie Dynamic Island je v iOS 26 čierne sklo s jemným odleskom (nie plne čierna farba) a vaše ikony musia mať pravdivú transparentnosť, nie čierne pozadie.
ActivityKit: ActivityAttributes a ContentState
Začnime od jadra. Každá Live Activity je definovaná typom, ktorý vyhovuje protokolu ActivityAttributes a obsahuje vnorenú štruktúru ContentState. Atribúty sú nemenné po štarte aktivity (napríklad ID objednávky alebo trasa), kým ContentState sa môže meniť počas celého života aktivity (napríklad odhadovaný čas príchodu alebo počet zostávajúcich krokov).
import ActivityKit
import SwiftUI
struct DeliveryAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var stage: Stage
var etaMinutes: Int
var courierName: String
enum Stage: String, Codable, Hashable {
case preparing, onTheWay, arriving, delivered
}
}
let orderNumber: String
let restaurantName: String
}
Aktivitu spustíte z aplikácie cez Activity.request(_:). Od iOS 16.2 je odporúčaný initializer s ActivityContent, ktorý oddeľuje stav od staleDate (čas, po ktorom iOS považuje obsah za zastaraný) a relevanceScore (priorita pri zobrazovaní viacerých aktivít):
Konfigurácia Live Activity sa deklaruje vo widget bundli pomocou ActivityConfiguration. Prvý closure definuje obsah uzamknutej obrazovky a banner; druhý parameter dynamicIsland definuje compact (vľavo a vpravo od kamery), minimal (keď beží viacero aktivít) a expanded (po dlhom podržaní) prezentácie. Toto je najdôležitejší kus kódu a zároveň miesto, kde najčastejšie vidím chyby, najmä že expanded layout nepoužíva všetky štyri DynamicIslandExpandedRegionPosition hodnoty.
Compact a minimal regióny majú prísny limit šírky, približne 44 bodov pre minimal a okolo 60 bodov pre každú stranu compact. Ak text presahuje, iOS ho jednoducho odreže bez akéhokoľvek varovania. Vždy používam .monospacedDigit() na číslice, aby sa layout nehýbal pri každej aktualizácii ETA. Pre časovače je lepšie siahnuť po Text(timerInterval:) alebo Text(_:style: .timer), ktoré systém sám aktualizuje raz za sekundu bez toho, aby ste vy posielali update.
Interaktívne Live Activities cez App Intents
Od iOS 17 môžu Live Activities obsahovať tlačidlá a Toggle, no len s akciami typu AppIntent. Bežný SwiftUI Button s closure sa vo widget procese nevykoná. Toto je miesto, kde sa Live Activities pretínajú s celým App Intents ekosystémom, ktorému sme sa venovali v sprievodcovi App Intents v iOS 26. Krátka verzia: intent musí byť definovaný v cieli, ktorý je shared medzi aplikáciou a widget extension, a jeho perform() musí byť dostatočne rýchly. Systém ho zabije po približne 30 sekundách.
Lokálne aktualizácie cez activity.update(_:) sú lacné, kým je aplikácia v popredí. Akonáhle ju používateľ zatvorí, iOS uplatní budget. V iOS 26 platí pre väčšinu kategórií zhruba 4 až 6 background aktualizácií za hodinu na aktivitu (Apple to formálne nedeklaroval, ale System Logs to potvrdzujú). Pre úlohy ako doručenie jedla, navigácia alebo športové skóre je preto reálne jediné riešenie push notifikácia s typom liveactivity.
Aktivita registrovaná s pushType: .token dostane na activity.pushToken stream byte tokenu, ktorý musíte poslať vášmu backendu. Payload má tvar:
Pri volaní APNs nastavte hlavičky apns-push-type: liveactivity a apns-topic: <bundle-id>.push-type.liveactivity. Bez týchto hlavičiek APNs vráti chybu 400 BadDeviceToken, aj keď je token správny. Detaily formátu nájdete v Apple dokumentácii pre ActivityKit push notifications.
Live Activities v režime StandBy
StandBy je režim aktivovaný, keď je iPhone na bezdrôtovej nabíjačke v horizontálnej polohe. V iOS 26 sa Live Activities v StandBy zobrazujú v štýle cez celú obrazovku, výrazne väčšie než lock screen prezentácia. Toto sa správa odlišne na iPade s Mac Catalyst (kde nie je vôbec) oproti iPhonu, a pre cross-platform aplikácie je potrebné to ošetriť. V mojich aplikáciách používam ten istý lock screen view, len s upravenými marginami:
Bez .contentMargins(.standBy, ...) obsah na okrajoch displeja orezáva zaoblený roh telefónu na nabíjačke. Apple v Human Interface Guidelines pre Live Activities odporúča aj zväčšenie typografie minimálne na 24 bodov pre StandBy. Z odstupu 2 až 3 metre cez izbu je predvolený 17-bodový text jednoducho nečitateľný.
Smart Stack na watchOS 26
Pre vývojárov je najväčšia zmena v iOS 26 to, že Live Activities sa automaticky prebíjajú na párovaný Apple Watch a zobrazia sa v Smart Stacku bez nutnosti samostatného watchOS targetu. Pred iOS 26 ste museli mať plnohodnotnú watchOS aplikáciu s vlastným WKApplication; teraz stačí jediný iPhone widget bundle a systém ho na hodinky prenesie cez Watch Connectivity vrstvu.
Layout pre Smart Stack sa berie z lock screen prezentácie, no s dvoma obmedzeniami: maximálna šírka je 396 bodov (Ultra) alebo 368 bodov (S10/S11), a interaktívne tlačidlá fungujú iba ak AppIntent nemá závislosť na iOS-only API. Ak váš intent volá UIApplication alebo SwiftData kontajner viazaný len na iPhone, na watchOS bude tlačidlo zobrazené ako disabled. V praxi navrhujem watchOS-safe verziu intentu cez compile-time check:
struct OpenOrderIntent: AppIntent {
static var title: LocalizedStringResource = "Otvoriť objednávku"
@Parameter(title: "ID")
var id: String
init() {}
init(id: String) { self.id = id }
func perform() async throws -> some IntentResult {
#if os(watchOS)
// No-op alebo otvor companion app cez handoff
return .result()
#else
await OrderRouter.shared.open(id: id)
return .result()
#endif
}
}
Ladenie a typické chyby
So, ktoré problémy sa opakujú v každom mojom code review Live Activities? Nasleduje môj osobný hit list:
„Activity nestartuje na simulátore.“ Live Activities fungujú v simulátore od Xcode 14.1, ale Dynamic Island sa zobrazí len pri simulácii iPhone 14 Pro a novších. Na iPhone 15+ simulátoroch zase chýba podpora StandBy bez prepnutia cez Features → Device → StandBy v menu Simulator.app.
„Aktualizácie prestanú prichádzať po pár minútach.“ Skontrolujte staleDate. Ak ho nenastavíte, iOS predpokladá 4 hodiny; ak ho nastavíte príliš krátko, aktivita zostane „stale“ a Dynamic Island ju skryje, aj keď push prichádza.
„Dynamic Island zobrazí len ikonu, nie expanded layout.“ iOS zobrazí compact iba ak je vaša aktivita najrelevantnejšia. Skúste zvýšiť relevanceScore alebo overiť, či neprekrýva natívnu aktivitu (napríklad časovač Hodín).
„AppIntent tlačidlo nič nerobí.“ Skontrolujte, či je intent dostupný cez App Intents target membership pre widget extension. Bežná chyba je, že intent je členom iba hlavnej aplikácie.
„Push payload prijatý, UI sa neaktualizuje.“ Zvyčajne nesprávne pole content-state. APNs neoznámi chybu validácie schémy a aktivita pôjde do stale stavu.
Na seriózny observability si na Activity<DeliveryAttributes>.activityUpdates a activity.pushTokenUpdates nasaďte logger cez Logger z unified logging systému a sledujte ich v Console.app. Tento konkrétny bug som chytil len vďaka tomu (token sa generoval, no backend ho ignoroval kvôli preklepu v Keychain accesse). Bez logov sa stratíte v tom, či token nedorazil, prišiel ale backend nepingol APNs, alebo APNs vrátilo chybu, ktorú váš server prehltol.
Často kladené otázky
Aký je rozdiel medzi Live Activity a klasickým widgetom?
Widget sa aktualizuje v plánovanom timeline maximálne raz za 15 až 30 minút a žije na home screen alebo lock screen. Live Activity beží len počas konkrétnej úlohy (maximálne 8 hodín, plus 4 hodiny v „stale“ stave), zobrazuje sa v Dynamic Island a aktualizuje sa real-time cez ActivityKit alebo APNs.
Ako dlho môže Live Activity trvať?
Aktívne maximálne 8 hodín od posledného update. Po dosiahnutí staleDate sa zobrazenie zmení na neaktívne, ale aktivita zostáva v UI ešte ďalšie 4 hodiny, kým ju systém alebo váš kód neukončí cez end(_:dismissalPolicy:).
Prečo sa moja Live Activity nezobrazuje v Dynamic Island?
Najčastejšie príčiny: zariadenie nemá Dynamic Island (iPhone 14 a staršie základné modely, iPad), iná aktivita má vyšší relevanceScore, alebo prebehol staleDate. Skontrolujte tiež, že máte definovaný compactLeading aj compactTrailing región, lebo bez oboch iOS prepadne na minimal.
Môžem spustiť Live Activity z push notifikácie?
Áno, od iOS 17.2 cez Activity.PushToStartToken a push payload s "event": "start". Predtým musela byť aplikácia otvorená aspoň raz. Push-to-start vyžaduje samostatné povolenie v Nastaveniach a token sa generuje cez Activity<Attributes>.pushToStartTokenUpdates.
Funguje Live Activity na iPade a Macu?
Na iPade áno od iPadOS 17. Zobrazí sa v notifikačnom centre a na uzamknutej obrazovke, ale bez Dynamic Island. Na macOS a v Mac Catalyst nie sú Live Activities natívne dostupné; aplikácie v Mac Catalyst module bežia bez Live Activity podpory, aj keď zdieľajú zdrojový kód s iOS verziou.
Ako pridať haptickú spätnú väzbu v SwiftUI cez SensoryFeedback a Core Haptics. Praktický sprievodca s hodnotami intenzity, AHAP súbormi a príkladmi z produkcie pre iOS 26.
Kompletný sprievodca prístupnosťou v SwiftUI pre iOS 26. Pokrýva VoiceOver, Dynamic Type, @ScaledMetric, accessibility traits, Reduce Motion, AccessibilityRotor a audio grafy v Swift Charts. S príkladmi kódu a praktickými tipmi z code review.
Praktický sprievodca tvorbou vlastných Swift makier. Od freestanding cez attached až po sedem rolí, SwiftSyntax, testovanie a debugovanie v Xcode 16.4.