Live Activity vor iOS-Throttling schützen (langsame Anzeige in CarPlay)

LevelActivityManager.update() stiess bei jeder Messung sofort
activity.update() an – der Neigungsmesser liefert per Notify oder
0,5s-Poll deutlich öfter als einmal pro Sekunde. Bei dieser Frequenz
drosselt iOS lokale Live-Activity-Aktualisierungen zunehmend selbst,
was in CarPlay auffiel: Das Dashboard zeigt neue Werte dort spürbar
verzögert an, weil es ohnehin zurückhaltender aktualisiert als
Sperrbildschirm oder Dynamic Island und die Drosselung dadurch sichtbar
wird.

Jetzt wird höchstens einmal pro Sekunde tatsächlich aktualisiert;
Messungen innerhalb der Sperrfrist werden nicht verworfen, sondern der
neueste Stand fürs Fristende vorgemerkt (wie schon beim
Verbindungsabbruch mit `pendingEnd` gehandhabt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
fototeddy
2026-09-06 21:39:08 +02:00
co-authored by Claude Sonnet 5
parent 59076076e8
commit ed026ce580
@@ -21,6 +21,23 @@ final class LevelActivityManager {
/// kurzen Funkloch aus und wieder ein.
static let disconnectGrace: TimeInterval = 12
/// Wie oft die Live Activity höchstens tatsächlich aktualisiert wird.
///
/// Der Neigungsmesser liefert Werte deutlich öfter (abonniert bei jeder
/// Änderung, sonst alle 0,5 s abgefragt). Stösst man ActivityKit im
/// selben Takt an, drosselt iOS nach kurzer Zeit selbst immer stärker
/// die Anzeige hinkt dann sichtbar hinterher, in CarPlay besonders
/// deutlich, weil das Dashboard ohnehin zurückhaltender aktualisiert als
/// Sperrbildschirm oder Dynamic Island. Ein fester Mindestabstand hält
/// die tatsächlichen Aktualisierungen unter dem, was das System duldet.
static let minimumUpdateInterval: TimeInterval = 1
@ObservationIgnored private var lastPushedAt: Date?
/// Ein während der Sperrfrist aufgelaufener Messwert, der nachgeholt
/// wird, sobald die Frist um ist sonst bliebe die Anzeige bei
/// schnellen Änderungen auf einem Zwischenstand stehen.
@ObservationIgnored private var pendingUpdate: Task<Void, Never>?
@ObservationIgnored private var pendingEnd: Task<Void, Never>?
/// Gerät, dessen Aktivität wegen einer länger anhaltenden Trennung
/// tatsächlich beendet wurde bei der nächsten Wiederverbindung wird sie
@@ -40,6 +57,9 @@ final class LevelActivityManager {
pendingEnd?.cancel()
pendingEnd = nil
deviceToResume = nil
pendingUpdate?.cancel()
pendingUpdate = nil
lastPushedAt = nil
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
endActivity()
let attributes = LevelActivityAttributes(deviceName: deviceName)
@@ -47,6 +67,7 @@ final class LevelActivityManager {
do {
activity = try Activity.request(attributes: attributes, content: content)
trackedDeviceID = deviceID
lastPushedAt = Date()
} catch {
// Kann z.B. an fehlender Nutzerfreigabe liegen dann bleibt es
// einfach bei der Anzeige in der App, es gibt sonst nichts zu tun.
@@ -55,8 +76,33 @@ final class LevelActivityManager {
/// Wird bei jeder neuen Messung aufgerufen, unabhängig davon, ob gerade
/// eine Aktivität läuft kein Aufwand ohne aktive Anzeige.
///
/// Innerhalb der Sperrfrist wird nicht einfach verworfen, sondern der
/// neueste Stand für ihr Ende vorgemerkt ein älterer vorgemerkter
/// Aufruf verfällt dabei. So kommt bei schnellen Änderungen immer der
/// zuletzt gemessene Wert an, nur eben gesammelt statt einzeln.
func update(deviceID: UUID, state: LevelState) {
guard trackedDeviceID == deviceID, let activity else { return }
guard trackedDeviceID == deviceID, activity != nil else { return }
pendingUpdate?.cancel()
pendingUpdate = nil
let elapsed = lastPushedAt.map { Date().timeIntervalSince($0) } ?? .infinity
guard elapsed < Self.minimumUpdateInterval else {
push(state)
return
}
let delay = Self.minimumUpdateInterval - elapsed
pendingUpdate = Task { [weak self] in
try? await Task.sleep(for: .seconds(delay))
guard let self, !Task.isCancelled, self.trackedDeviceID == deviceID else { return }
self.pendingUpdate = nil
self.push(state)
}
}
private func push(_ state: LevelState) {
guard let activity else { return }
lastPushedAt = Date()
let content = ActivityContent(state: Self.contentState(from: state), staleDate: nil)
Task { await activity.update(content) }
}
@@ -100,6 +146,8 @@ final class LevelActivityManager {
}
private func endActivity() {
pendingUpdate?.cancel()
pendingUpdate = nil
guard let activity else { return }
trackedDeviceID = nil
self.activity = nil