Live Activities to maszyna stanów, a nie odznaka
Gatunek: shipped-code. Wpis dokumentuje Live Activity, którą zbudowałem w Return — timerze medytacyjnym napisanym w SwiftUI, używanym przez moją żonę, moją mamę i kilka tysięcy nieznajomych.1 Wzorce, które tu przedstawiam, to te, które przetrwały produkcję. Stopka z brutalną szczerością mówi, czego jeszcze nie wiem.
Live Activity w Return wygląda jak liczba odliczająca czas na ekranie blokady i na Dynamic Island.2 To nie jest liczba. To pięciostanowa maszyna cyklu życia z trzema zewnętrznymi ścieżkami zamykania i jedną ścieżką wejściową ponownego startu, która musi się bronić sama przed sobą.
Wysłałem v1, która traktowała Live Activity jak odznakę. „Aktualny pozostały czas” był danymi; reszta była dekoracją. Ta wersja miała trzy błędy, które wyłapałem w TestFlight, i jeden, który wyłapałem na produkcji:
- Naciśnięcie startu, gdy start był już w toku, tworzyło drugą aktywność, która osierocała pierwszą.
- Odliczanie renderowało się poprawnie na Dynamic Island, ale widok ekranu blokady trafiał na
endTime <= Date()dla wstrzymanych timerów i pokazywał0:00, dopóki użytkownik nie wznowił. - Live Activity pozostawała widoczna długo po tym, jak użytkownik zresetował timer, ponieważ polityka zamykania to było
.default, którą Apple utrzymuje widoczną przez pewien czas, do czterech godzin. - (Produkcja.) W lokalizacjach języków pisanych od prawej do lewej (arabski, hebrajski) cyfry renderowały się odwrotnie w obszarze compact-trailing Dynamic Island. Łacińskie cyfry, układ RTL. Poprawka miała jedną linię.
Każdy z nich był błędem maszyny stanów. Liczba odliczania była w porządku. Liczba nie jest produktem. Produktem jest stan.
Maszyna stanów poniżej to ta, która przetrwała te błędy.
TL;DR
- Wysłany
LiveActivityManagerudostępnia 5 metod przejścia (startActivity,updateActivity,showCycleComplete,showFinalCompletion,endActivity) plus 1 odczyt (hasActiveActivity). 224 linie produkcyjne chronią jedno konkretne zagrożenie wewnątrzstartActivity: równoczesne wywołania startu plus sprawdzenia anulowania na każdej granicyawaitw tej metodzie.3 ContentStateniesie 6 pól:endTime,currentCycle,totalCycles,isPaused,isCompleted,remainingSeconds. Pierwsze pięć to etykiety maszyny stanów. Szóste (remainingSeconds) jest statycznym fallbackiem wyświetlania, którego livetimerIntervalz ActivityKit nie może obsłużyć.- Decyzja dotycząca polityki zamykania to prawdziwy wybór produktowy.
.immediatedla resetu przez użytkownika,.after(Date().addingTimeInterval(3))dla zakończenia, nigdy domyślne ustawienie systemowe. - Obszar compact-trailing Dynamic Island wymaga
.environment(\.layoutDirection, .leftToRight)na tekście timera, aby utrzymać łacińskie cyfry w układzie LTR przy systemowych ustawieniach RTL.
Maszyna stanów
Wysłana Live Activity ma jeden stan bezczynny, trzy stany aktywne, które użytkownik może obserwować, jeden stan końcowy i jedną bramkę ponownego wejścia, którą deweloper musi obserwować:
┌──────────────────────────────────────────────────────────────────┐
│ Lifecycle states │
├──────────────────────────────────────────────────────────────────┤
│ IDLE currentActivity == nil; no Live Activity present │
│ RUNNING isPaused=false, endTime > Date() │
│ PAUSED isPaused=true, remainingSeconds=N │
│ CYCLE_END isPaused=false, endTime <= Date(), isCompleted=false│
│ COMPLETE isCompleted=true (terminal; transitions to IDLE) │
└──────────────────────────────────────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────────┐
│ Dismissal policies (Apple) │
├──────────────────────────────────────────────────────────────────┤
│ .immediate user reset │
│ .after(now + 3s) completion display window │
│ .default system decides; can stay up to 4 hours │
└──────────────────────────────────────────────────────────────────┘
Reentrancy gate inside startActivity():
isStartingActivity flag + cancellable startActivityTask
prevents two concurrent startActivity() calls from creating
two Live Activities for one timer. Cancellation checks across
each await keep the in-flight task safe to abort.
Ścieżka renderowania sprawdza isPaused jako pierwszą; ta kolejność zapobiega temu, by wstrzymany timer renderował się jako CYCLE_END, gdy czas zegarowy przekroczył endTime.7
Nazwy stanów nie są etykietami liczby. Nazwy stanów są kontraktem między LiveActivityManager (strona aplikacji, gdzie żyją moje widoki SwiftUI) a ReturnLiveActivity (rozszerzeniem widgetu, gdzie proces Apple renderuje powierzchnię).
Kontraktem jest TimerActivityAttributes.ContentState, wszystkie 6 pól:3
public struct ContentState: Codable, Hashable {
var endTime: Date
var currentCycle: Int
var totalCycles: Int?
var isPaused: Bool
var isCompleted: Bool = false
var remainingSeconds: Int = 0
}
Każde przejście stanu mutuje tę strukturę i prosi ActivityKit, by dostarczył ją przez granice procesów do rozszerzenia widgetu. Widget następnie renderuje się ponownie. Nie ma pamięci współdzielonej. Nie ma callbacku. Jest struktura Codable, która przekracza granicę procesu przy każdym przejściu.
Ten fakt wyklucza wszystko, co mógłbym chcieć zrobić z domknięciami, view modelami, observable objects czy właściwościami obliczanymi. Stan musi dawać się wyrazić jako dane serializowalne. Jeśli nie da się go zakodować, nie może przejść.
Ponowny start
Live Activities mają twardy limit równoczesnych aktywności i miękki limit dotyczący tego, co się stanie, jeśli wywoła się Activity.request dwa razy w trakcie wykonywania. Twardy limit jest dobrze udokumentowany.4 Miękki limit brzmi: „drugie wywołanie może się powieść i utworzyć sierotę”. Sierota to Live Activity, która nie jest już powiązana z currentActivity w menedżerze. Przeżywa. Nie ma ścieżki powrotnej do kodu. Sama się zamyka po jakimś czasie, gdy zegar nieświeżości zadecyduje. Użytkownik widzi zduplikowany timer do tego momentu.
Sierota była błędem v1, którą Return wysłał. Poprawką jest bramka ponownego wejścia plus anulowalny Task w LiveActivityManager.swift:3
private var isStartingActivity = false
private var startActivityTask: Task<Void, Never>?
func startActivity(...) {
#if os(iOS)
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
guard !isStartingActivity else { return }
isStartingActivity = true
startActivityTask?.cancel()
startActivityTask = Task {
defer {
isStartingActivity = false
startActivityTask = nil
}
guard !Task.isCancelled else { return }
await endActivity() // explicit cleanup of any prior state
guard !Task.isCancelled else { return }
// ... build attributes + contentState ...
do {
let activity = try Activity.request(...)
guard !Task.isCancelled else { return }
currentActivity = activity
} catch {
// log; flag clears via defer
}
}
#endif
}
Trzy rzeczy dotyczące tego wzorca, których dokumentacja nie wskazuje:
Flaga isStartingActivity to aktywna ochrona; startActivityTask?.cancel() to defensywne sprzątanie. Flaga zwarcie blokuje każde drugie wywołanie startActivity, gdy pierwsze jest w toku, więc tak naprawdę nie ścigasz się na publicznej ścieżce. Taniec cancel-then-replace nadal ma znaczenie, bo Task wykonujący się asynchronicznie może przeżyć krótko żyjącego wywołującego; anulowanie zapobiega kontynuacji nieaktualnego Tasku po tym, jak wywołujący poszedł dalej.
Sprawdzenia guard !Task.isCancelled na każdej granicy await. Anulowanie w Swifcie jest kooperatywne. Nawet jeśli wywołane zostanie cancel, Task działa dalej, dopóki sam tego jawnie nie sprawdzi. Każde await jest okazją do sprawdzenia. Bez sprawdzeń po await anulowany Task nadal buduje stan aktywności, wywołuje Activity.request i po cichu tworzy sierotę przy powodzeniu.
defer czyści flagę zanim ciało Tasku się zakończy. Bez defer wczesne return (z kontroli anulowania) pozostawia isStartingActivity = true na stałe i aktywność nigdy więcej się nie uruchomi do czasu ponownego uruchomienia aplikacji. Flaga to blokada; blokada musi się zwalniać na każdej ścieżce wyjścia.
Argument pushType: nil. Return nie używa aktualizacji Live Activity wysyłanych przez APNs. Aplikacja aktualizuje aktywność lokalnie przez activity.update. Jeśli potrzebne są aktualizacje sterowane pushami (śledzenie dostawy, wyniki sportowe, dane w czasie rzeczywistym), typem jest pushType: .token, a kontrakt jest dramatycznie bardziej złożony.5 Aktualizacje lokalne są prostsze i pokrywają każdy timer / licznik / przepływ jednoaplikacyjny.
Problem z pauzą
ActivityKit dostarcza piękny widok Text(timerInterval: Date()...endTime, countsDown: true), który renderuje aktywne odliczanie bez żadnej aktualizacji ze strony aplikacji.6 Ustawia się czas zakończenia, system renderuje aktywny timer. Bez Timer.publish, bez odświeżania widgetu, bez drenowania baterii.
To fantastyczne, gdy timer działa. To błąd, gdy timer jest wstrzymany.
Tekst timerInterval odlicza w stronę endTime niezależnie od jakiegokolwiek sygnału „pauzy” w stanie. Nie ma trybu „zamrożone na 10:23” w API Apple. Jeśli przekażesz endTime = Date().addingTimeInterval(623) i użytkownik wstrzyma na znaczniku 10:23, tekst timera nadal odlicza do zera w widgecie. Pole stanu mówi paused. Widget renderuje running.
Poprawką jest renderowanie dwóch różnych widoków z tego samego stanu:7
if context.state.isPaused {
// static text
Text(formatTime(context.state.remainingSeconds))
.monospacedDigit()
} else if context.state.endTime > Date() {
// live countdown
Text(timerInterval: Date()...context.state.endTime, countsDown: true)
.monospacedDigit()
} else {
// post-end static
Text("0:00")
.monospacedDigit()
}
Renderowanie dwutorowe to powód, dla którego ContentState niesie remainingSeconds jako oddzielne pole. Jest zbędne, gdy timer działa (system oblicza je z endTime). Jest jedynym źródłem prawdy, gdy timer jest wstrzymany. Dwie połowy struktury służą dwóm różnym trybom renderowania; boolean isPaused wybiera między nimi.
Polityki zamykania
activity.end(_:dismissalPolicy:) przyjmuje jedną z trzech wartości ActivityUIDismissalPolicy, a zły wybór sprawił, że moja v1 pozostawała na ekranie blokady użytkownika przez czas, który wydawał się wiecznością po resecie:13
| Polityka | Kiedy używać | Co otrzymujesz |
|---|---|---|
.immediate |
Reset użytkownika, błąd, aplikacja w tle bez aktywności do śledzenia | Aktywność znika natychmiast. Bez okna karencji |
.after(date) |
Wyświetlenie zakończenia: „twoja medytacja jest zakończona” musi być czytelne przez chwilę. Data musi mieścić się w czterogodzinnym oknie, na które pozwala Apple | Aktywność pokazuje końcowy stan, a następnie zamyka się o date |
.default |
Gdy naprawdę chce się, by heurystyki Apple zadecydowały | System utrzymuje ją widoczną „przez pewien czas” (sformułowanie Apple), do czterech godzin po wywołaniu end |
Return używa .after(Date().addingTimeInterval(3)) dla naturalnej ścieżki zakończenia:3
await activity.end(
.init(state: contentState, staleDate: nil),
dismissalPolicy: .after(Date().addingTimeInterval(3))
)
Trzy sekundy to czas, którego użytkownik potrzebuje, by spojrzeć na ekran blokady, zarejestrować, że timer się zakończył, i poczuć satysfakcję z odhaczenia. Mniej niż trzy jest nerwowe. Więcej niż trzy sprawia wrażenie, że aktywność nie wie, że jest zakończona.
Dla resetu wywołanego przez użytkownika wywołanie to dismissalPolicy: .immediate. Bez okna. Użytkownik już wie.
Złym wyborem w v1 było .default. Dla zakończonego timera medytacyjnego system utrzymywał aktywność widoczną wystarczająco długo, by użytkownicy myśleli, że aplikacja w ogóle nie zarejestrowała zakończenia. Dokumentacja Apple mówi, że .default utrzymuje zakończoną aktywność „widoczną przez pewien czas” do czterech godzin;13 właściwą postawą dla timera jest uczynienie zamknięcia jawnym.
Obszar compact Dynamic Island
Dynamic Island ma trzy tryby renderowania i potrzebne są wszystkie trzy nawet dla prostego timera:2
- Compact (domyślny kształt Dynamic Island): wiodąca ikona + zamykający timer
- Minimal (gdy inna Live Activity konkuruje o tę samą Dynamic Island): tylko wiodąca ikona
- Expanded (długie naciśnięcie): cztery nazwane obszary (
leading,trailing,center,bottom)
Wzorzec, który zasłużył na swoje miejsce w Return, polega na uczynieniu widoku rozwiniętego niemal identycznym z compactem:8
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image("AppIconSmall")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
DynamicIslandExpandedRegion(.trailing) {
TimerText(...)
}
DynamicIslandExpandedRegion(.center) { EmptyView() }
DynamicIslandExpandedRegion(.bottom) { EmptyView() }
} compactLeading: {
Image("AppIconSmall")...
} compactTrailing: {
TimerText(...)
} minimal: {
Image("AppIconSmall")...
}
Większość tutoriali Live Activity skłania się ku widokowi rozwiniętemu jako „prawdziwemu” projektowi, z bogatą zawartością w obszarze bottom. Dla timera medytacyjnego rozwinięcie jest balastem. Użytkownik otwiera widok rozwinięty długim naciśnięciem, a długie naciśnięcie samo daje mu haptyczną informację, że coś się stało. Dodanie zawartości sprawia, że rozwinięcie mówi coś, o co użytkownik nie prosił. Puste obszary w trybie rozwiniętym nie są porażką projektu; są projektem.
Błąd RTL
Błąd produkcyjny. Użytkownicy arabscy i hebrajscy na iOS zgłaszali, że timer compact-trailing na Dynamic Island renderował cyfry odwrotnie. Łaciński ciąg cyfr 5:23 renderował się jako 32:5, ponieważ kierunek układu compact-trailing dziedziczył ustawienie RTL z lokalizacji systemowej.
SwiftUI dziedziczy systemowy kierunek układu wewnątrz procesu widgetu, więc tekst timera Dynamic Island przyjmował RTL, gdy telefon użytkownika był ustawiony na arabski lub hebrajski. Cyfry łacińskie powinny renderować się LTR nawet wewnątrz interfejsu RTL. Poprawką jest przypięcie kierunku układu na widokach tekstów numerycznych:7
.environment(\.layoutDirection, .leftToRight)
Override idzie na widoki numeryczne Text wewnątrz TimerText (Dynamic Island compact / expanded) oraz wewnątrz widoku ekranu blokady, a nie na cały widok. Cyfry łacińskie czyta się od lewej do prawej niezależnie od lokalizacji systemowej użytkownika; etykiety cykli takie jak „Cycle 2 of 3” pozostają zlokalizowane, więc podążają za systemowym kierunkiem układu.
Błąd nie ujawnia się w domowej lokalizacji TestFlight. Ujawnia się w momencie, gdy prawdziwy użytkownik RTL otworzy timer. Lekcja: wysyłaj override środowiska przypiętego do LTR na każdym widoku tekstu z cyframi łacińskimi w każdej Live Activity, która może działać w lokalizacjach RTL.
Historia lokalizacji
TimerActivityAttributes niesie pole languageCode: String ustawiane przez aplikację przy tworzeniu aktywności:9
let attributes = TimerActivityAttributes(
timerDuration: duration,
languageCode: settings.appLanguage // app's selected language, not system's
)
Rozszerzenie widgetu czyta to, by renderować zlokalizowane ciągi:
private var locale: Locale {
let code = context.attributes.languageCode
return code.isEmpty ? .current : Locale(identifier: code)
}
private func localized(_ key: String.LocalizationValue) -> String {
String(localized: key, locale: locale)
}
Dlaczego aplikacja przekazuje swój własny kod języka zamiast pozwolić widgetowi czytać Locale.current: rozszerzenie widgetu działa we własnym procesie. Jego Locale.current to lokalizacja systemowa, nie wybrana lokalizacja aplikacji. Jeśli użytkownik ustawił Return na koreański, podczas gdy iPhone jest po angielsku, widget mówiłby po angielsku bez tego override’u. Preferencja językowa aplikacji podróżuje w atrybutach aktywności; widget ją honoruje.
Localizable.xcstrings żyje w celu (target) widgetu obok aplikacji, ale to oddzielne pliki. Ciągi używane w widgecie muszą istnieć w ReturnWidgets/Localizable.xcstrings, nawet jeśli ten sam ciąg istnieje w Return/Localizable.xcstrings. Zapomnienie o tym oznacza, że widget wraca do języka deweloperskiego, podczas gdy aplikacja mówi po koreańsku.
Co bym zrobił inaczej
Zmniejszyć ContentState. Sześć pól to za dużo. Redundancja między endTime i remainingSeconds to cena za obejście braku trybu pauzy w timerInterval. Gdybym zaczynał od nowa, niósłbym pojedynczą enumerację displayMode (running, paused(remainingSeconds: Int), cycleEnd, complete) i pozwolił, by kod renderujący rozsyłał na podstawie przypadku. Sześć pól jest trudniej utrzymać poprawnie zmutowanych przez pięć metod przejścia niż cztery przypadki.
Dodać interaktywne przyciski Live Activity (iOS 17+). Return obecnie nie udostępnia kontrolek pauzy/wznowienia w Dynamic Island. Użytkownik musi otworzyć aplikację, by wstrzymać. iOS 17 dodał Button(intent:) dla App Intents wewnątrz Live Activities.10 Interaktywna kontrolka pauzy to oczywiste rozszerzenie i następna rzecz, którą wyślę dla Return.
Live Activities aktualizowane przez push dla synchronizacji timera między urządzeniami. Return synchronizuje sesje między iPhone, iPad, Watch i Apple TV przez NSUbiquitousKeyValueStore (omówione w Pięć platform Apple, trzy współdzielone pliki). Dziś aktywność jest uruchamiana lokalnie z aplikacji iPhone lub iPad i aktualizowana lokalnie. Użytkownik uruchamiający timer na Apple Watch mógłby idealnie zobaczyć Live Activity odzwierciedloną na iPhonie w czasie rzeczywistym. Push APNs do Live Activity to ścieżka.5 Jeszcze nie zbudowałem.
Kiedy nie używać Live Activities
Jednorazowy stan przejściowy. Toast „zapisano!” nie zasługuje na Live Activity. System ma baner. Użyj go.
Często zmieniające się dane bez wymiaru timera. Live Activities działają najlepiej dla rzeczy z wyraźną kotwicą czasową (timer, ETA dostawy, zegar gry, czas trwania połączenia telefonicznego). Tickery giełdowe i wyniki sportowe działają, ponieważ mają okno sesji. Ogólny dashboard nie.
Aplikacje bez przypadku użycia ekranu blokady / standby. Live Activities wymagają realnej inwestycji inżynieryjnej (konfiguracja celu, projekt ContentState, decyzje o polityce zamykania, obsługa RTL, infrastruktura lokalizacji). Aplikacje, które użytkownik otwiera bezpośrednio, nigdy nie konsultując ekranu blokady podczas użycia, nie mają odpowiedniego kształtu. Edytor zdjęć nie potrzebuje. Tracker treningu — tak.
Na powierzchniach poza iOS, z zastrzeżeniami. LiveActivityManager w Return wysyła swoją implementację za #if os(iOS), ponieważ timer jest uruchamiany z aplikacji iPhone lub iPad. Sam ActivityKit opisuje baner ekranu blokady, Dynamic Island, Apple Watch Smart Stack, Mac i CarPlay jako powierzchnie prezentacji; iOS 26 rozszerzył kilka z nich.4 watchOS nadal ma swoje własne komplikacje API dla pełnoekranowego renderowania. macOS ma aplikacje paska menu. iPadOS wspiera Live Activities od iPadOS 17 bez obszaru Dynamic Island. Menedżer Return ma 8 zabezpieczeń #if os(iOS) w jednym pliku 224 linii.
Co wzorzec oznacza dla aplikacji wysyłanych na iOS 26+
Dwa wnioski.
-
Traktuj Live Activity jako maszynę stanów, a nie liczbę. Maszyna stanów ma jasne stany, jasne przejścia i jasne reguły zamykania. Liczba na ekranie to jedno renderowanie jednego stanu. Najpierw popraw stany.
-
Zabezpieczenie ponownego wejścia to błąd, na który jeszcze nie trafiłeś. Każdy menedżer Live Activity, jaki widziałem na wolności, który nie implementuje
isStartingActivity+ anulowalnego Tasku, wysłał co najmniej jeden błąd osieroconej aktywności. Zabezpieczenie ma 6 linii. Napisz je raz.
Sparuj ten wpis z moimi wcześniejszymi opisami dla tej samej rodziny aplikacji: typowane App Intents dla Apple Intelligence; serwery MCP dla agentów cross-LLM; wzorce Liquid Glass dla warstwy wizualnej; wysyłka wieloplatformowa dla zasięgu między urządzeniami. Live Activities to warstwa Lock Screen i Dynamic Island na iOS tego samego stosu. Pełny zestaw mieszka w hubie serii Apple Ecosystem. Dla szerszego kontekstu iOS-z-agentami-AI zobacz przewodnik iOS Agent Development.
FAQ
Jaka jest różnica między Live Activities a widgetami WidgetKit?
Widgety WidgetKit renderują się w odstępach zdefiniowanych przez TimelineProvider; system decyduje, kiedy odświeżyć, a widget renderuje się ponownie ze statycznej osi czasu.11 Live Activities renderują się w odpowiedzi na konkretne wywołania activity.update(...) sterowane przez aplikację i żyją przez czas trwania podstawowej aktywności (timera, dostawy, treningu). Oba wysyłają w celu rozszerzenia widgetu; różnica leży w modelu wyzwalacza.
Czy Live Activities działają na iPadzie?
Tak, w iPadOS 17+. Baner ekranu blokady jest podstawową powierzchnią renderowania; iPad nie ma Dynamic Island. Ten sam kod ActivityConfiguration działa; należy tylko oczekiwać, że obszary Dynamic Island nigdy nie zostaną wyrenderowane na iPadzie.
Czy Live Activity może przeżyć proces mojej aplikacji?
Tak. Gdy Activity.request się powiedzie, ActivityKit jest właścicielem aktywności. Proces aplikacji może zostać zakończony przez system; aktywność nadal renderuje się na ekranie blokady i Dynamic Island, dopóki jawnie jej nie zakończysz (lub dopóki systemowe reguły nieświeżości jej nie zamkną). Jawne wywołania endActivity() mają z tego powodu znaczenie; bez jawnego zakończenia przy resecie aplikacji aktywność przeżyje timer.
Dlaczego wpis nie omawia Live Activities aktualizowanych przez push?
Nie wysłałem Live Activities aktualizowanych przez push w Return. Zgodnie z regułą gatunku dla tego klastra: wpisy shipped-code dokumentują tylko to, co robi kod produkcyjny. Aktualizacje push są wymienione w „Co bym zrobił inaczej”; przyszły wpis omówi je po tym, jak je wyślę.
Jaki jest faktyczny układ plików dla Live Activities w aplikacji SwiftUI?
- W głównym celu aplikacji:
LiveActivityManager.swift(zarządza cyklem życia aktywności),TimerActivityAttributes.swift(strukturaActivityAttributeswspółdzielona z widgetem; oba cele kompilują ten plik). - W celu rozszerzenia widgetu:
ReturnLiveActivity.swift(zgodność zWidgetz ciałemActivityConfiguration),ReturnWidgetsBundle.swift(@main WidgetBundle). - Konfiguracja:
Info.plistzNSSupportsLiveActivities = YESw celu aplikacji.
Cel rozszerzenia widgetu potrzebuje importów ActivityKit i WidgetKit. TimerActivityAttributes to jedyny plik współdzielony między oboma celami; wszystko inne jest izolowane do celu.
Live Activity to nie liczba na ekranie blokady. To maszyna stanów, która przekracza granicę procesu przy każdym przejściu. Popraw stany, zabezpiecz ponowne wejście, wybierz politykę zamykania świadomie i przypnij kierunek układu. Liczba zatroszczy się o siebie sama.
Bibliografia
-
Autorski Return, timer medytacyjny SwiftUI opublikowany w App Store 21 kwietnia 2026, dostępny na iPhone, iPad, Mac, Apple Watch i Apple TV. Live Activities wysyłają się tylko na cel iOS. ↩
-
Apple Developer, „ActivityKit framework”. Baner ekranu blokady, tryby compact / minimal / expanded Dynamic Island, cykl życia aktywności. Dostępne iOS 16.1+; Dynamic Island dostępne na iPhone 14 Pro i nowszych. ↩↩
-
Kod produkcyjny w
Return/Return/LiveActivityManager.swift(224 linie, 8 bloków#if os(iOS)) iReturn/Return/TimerActivityAttributes.swift(43 linie). Współdzielone między celem aplikacji a celem rozszerzenia widgetu poprzez członkostwo w celu. ↩↩↩↩↩ -
Apple Developer, „Displaying live data with Live Activities”. Limity równoczesności, wspierane platformy (iOS 16.1+, iPadOS 17+), klucz
NSSupportsLiveActivitiesw Info.plist. ↩↩ -
Apple Developer, „Updating and ending your Live Activity with ActivityKit push notifications”. Ścieżka
pushType: .tokenwymaga oddzielnego klucza autoryzacyjnego APNs, rejestracji tokenu push po stronie serwera i innego protokołu aktualizacji niż lokalne wywołaniaactivity.update(...). ↩↩ -
Apple Developer, „Text(timerInterval:pauseTime:countsDown:showsHours:)”. Aktywny timer odliczania renderowany przez system; renderuje się bez aktualizacji aplikacji, gdy aktywność jest uruchomiona. ↩
-
Kod produkcyjny w
Return/ReturnWidgets/ReturnLiveActivity.swift(232 linie). ZgodnośćWidgetrozszerzenia widgetu z ciałemActivityConfiguration<TimerActivityAttributes>. WidokTimerTextw liniach 61-102 obsługuje renderowanie trzystanowe paused / running / post-end. ↩↩↩↩ -
Apple Developer, „DynamicIsland”. Cztery nazwane obszary rozwinięte (
leading,trailing,center,bottom) plus trzy widoki trybu compact (compactLeading,compactTrailing,minimal). ↩ -
Rozszerzenie widgetu działa we własnym procesie i dziedziczy lokalizację systemową, nie wybraną lokalizację aplikacji. Aplikacje wspierające przełączanie języka w aplikacji (Return wspiera 27 języków) muszą przekazać kod języka przez
ActivityAttributes, by widget mógł renderować się w wybranym przez użytkownika języku. Wzorzec:Locale(identifier: context.attributes.languageCode)zamiastLocale.current. ↩ -
Apple Developer, „Button(intent:)”. Dostępne w widoki widgetu i Live Activity od iOS 17+. Mostkuje App Intents do kontrolek ekranu blokady / Dynamic Island bez wymagania uruchomienia aplikacji na pierwszym planie. ↩
-
Apple Developer, „TimelineProvider”. Model odświeżania widgetu poprzedzający Live Activities; wstępnie obliczone wpisy z oknami przeładowania zarządzanymi przez system. ↩
-
Kod produkcyjny w
Return/ReturnWidgets/ReturnWidgetsBundle.swift(16 linii).@main WidgetBundle, który rejestrujeReturnLiveActivityjako jedyny widget rozszerzenia widgetu. Wymagany wzorzec dla rozszerzeń widgetu; bundle jest tym, co system ładuje. ↩ -
Apple Developer, „ActivityUIDismissalPolicy”. Trzy przypadki:
.default,.immediate,.after(_:). Apple stwierdza, że.defaultutrzymuje zakończoną Live Activity widoczną „przez pewien czas” do czterech godzin, a.after(_:)przyjmuje datę w tym samym czterogodzinnym oknie. ↩↩