CarPlay renders Live Activities through the .small activity family (shared with the Watch Smart Stack), not the lock-screen content view. Without it, CarPlay fell back to the Dynamic Island's compactLeading/ compactTrailing, which only had the generic "level" SF Symbol and an empty trailing view — hence the wrong icon and missing text. Adds .supplementalActivityFamilies([.small]) and a compact layout that reuses the real LevelBubbleGlyph plus a one-line instruction for that family, while the lock screen keeps its existing two-line view. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
67 lines
2.7 KiB
Swift
67 lines
2.7 KiB
Swift
import ActivityKit
|
||
import Foundation
|
||
import Observation
|
||
|
||
/// Startet, aktualisiert und beendet die Live Activity des Neigungsmessers.
|
||
///
|
||
/// Es läuft höchstens eine Aktivität gleichzeitig – mehr als einen
|
||
/// Neigungsmesser gibt es im aktiven Profil ohnehin nicht. Seit iOS 26 zeigt
|
||
/// CarPlay eine laufende Live Activity automatisch im Dashboard an; ein
|
||
/// eigenes CarPlay-App-Target braucht es dafür nicht – die Inhalte kommen
|
||
/// aber nicht von der Sperrbildschirm-Ansicht, sondern von der
|
||
/// `.small`-Aktivitätsfamilie (siehe `VanAligneiOSWidgetLiveActivity`).
|
||
@Observable
|
||
final class LevelActivityManager {
|
||
private(set) var trackedDeviceID: UUID?
|
||
@ObservationIgnored private var activity: Activity<LevelActivityAttributes>?
|
||
|
||
/// Ob gerade irgendeine Aktivität läuft – die App muss dafür im
|
||
/// Hintergrund weiter nach dem Neigungsmesser funken, sonst friert die
|
||
/// Anzeige beim ersten Sperren des Bildschirms ein.
|
||
var isActive: Bool { trackedDeviceID != nil }
|
||
|
||
func isActive(for deviceID: UUID) -> Bool {
|
||
trackedDeviceID == deviceID && activity != nil
|
||
}
|
||
|
||
func start(deviceID: UUID, deviceName: String, state: LevelState) {
|
||
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
|
||
end()
|
||
let attributes = LevelActivityAttributes(deviceName: deviceName)
|
||
let content = ActivityContent(state: Self.contentState(from: state), staleDate: nil)
|
||
do {
|
||
activity = try Activity.request(attributes: attributes, content: content)
|
||
trackedDeviceID = deviceID
|
||
} catch {
|
||
// Kann z.B. an fehlender Nutzerfreigabe liegen – dann bleibt es
|
||
// einfach bei der Anzeige in der App, es gibt sonst nichts zu tun.
|
||
}
|
||
}
|
||
|
||
/// Wird bei jeder neuen Messung aufgerufen, unabhängig davon, ob gerade
|
||
/// eine Aktivität läuft – kein Aufwand ohne aktive Anzeige.
|
||
func update(deviceID: UUID, state: LevelState) {
|
||
guard trackedDeviceID == deviceID, let activity else { return }
|
||
let content = ActivityContent(state: Self.contentState(from: state), staleDate: nil)
|
||
Task { await activity.update(content) }
|
||
}
|
||
|
||
func end() {
|
||
guard let activity else { return }
|
||
trackedDeviceID = nil
|
||
self.activity = nil
|
||
Task { await activity.end(nil, dismissalPolicy: .immediate) }
|
||
}
|
||
|
||
private static func contentState(from state: LevelState) -> LevelActivityAttributes.ContentState {
|
||
LevelActivityAttributes.ContentState(
|
||
pitch: state.pitch,
|
||
roll: state.roll,
|
||
isLevel: state.isLevel,
|
||
instruction: state.instruction,
|
||
isCalibrated: state.isCalibrated,
|
||
updatedAt: Date()
|
||
)
|
||
}
|
||
}
|