- Assign the shared development team to the widget extension and watch complication targets, and correct the watch app's companion bundle identifier so on-device installs succeed after the bundle ID rename to de.s0.fototeddy.VanAligneiOS. - Guard against two ConfiguredDevices sharing a peripheralID (e.g. a double-tapped "Sichern"), which crashed BluetoothManager's Dictionary(uniqueKeysWithValues:) on launch. - Keep Bluetooth running in the background while a Live Activity is active, so lock screen/CarPlay keep receiving fresh readings instead of freezing at the last value before backgrounding. - Render the actual bubble-level graphic (not just a static icon) in the Live Activity's lock screen view and expanded Dynamic Island. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
65 lines
2.6 KiB
Swift
65 lines
2.6 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 17 zeigt
|
||
/// CarPlay eine laufende Live Activity automatisch im Dashboard an; ein
|
||
/// eigenes CarPlay-App-Target braucht es dafür nicht.
|
||
@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()
|
||
)
|
||
}
|
||
}
|