From 004e64e933775481aefe769694099bdc7e97d639 Mon Sep 17 00:00:00 2001 From: fototeddy Date: Sat, 5 Sep 2026 11:37:50 +0200 Subject: [PATCH] Fix Live Activity in CarPlay: show real bubble and text via .small family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../LiveActivity/LevelActivityManager.swift | 6 +- .../VanAligneiOSWidgetLiveActivity.swift | 56 +++++++++++++++++-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/CamperMonitor/LiveActivity/LevelActivityManager.swift b/CamperMonitor/LiveActivity/LevelActivityManager.swift index 6ffafbd..caee0eb 100644 --- a/CamperMonitor/LiveActivity/LevelActivityManager.swift +++ b/CamperMonitor/LiveActivity/LevelActivityManager.swift @@ -5,9 +5,11 @@ 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 +/// 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. +/// 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? diff --git a/VanAligneiOSWidget/VanAligneiOSWidgetLiveActivity.swift b/VanAligneiOSWidget/VanAligneiOSWidgetLiveActivity.swift index e33b655..00d49a7 100644 --- a/VanAligneiOSWidget/VanAligneiOSWidgetLiveActivity.swift +++ b/VanAligneiOSWidget/VanAligneiOSWidgetLiveActivity.swift @@ -81,7 +81,7 @@ private struct LevelBubbleGlyph: View { struct VanAligneiOSWidgetLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: LevelActivityAttributes.self) { context in - LockScreenLevelView(attributes: context.attributes, state: context.state) + LevelActivityContent(attributes: context.attributes, state: context.state) .activityBackgroundTint(Color(white: 0.08)) .activitySystemActionForegroundColor(.white) @@ -109,12 +109,60 @@ struct VanAligneiOSWidgetLiveActivity: Widget { } .keylineTint(context.state.isLevel ? .green : .orange) } + .supplementalActivityFamilies([.small]) } } -/// Sperrbildschirm- und Banner-Ansicht. Dieselbe Ansicht landet automatisch -/// auch im CarPlay-Dashboard, sobald das iPhone verbunden ist – ein eigenes -/// CarPlay-App-Target ist dafür nicht nötig. +/// Wählt je nach Darstellungsgrösse zwischen Sperrbildschirm- und +/// Kompaktansicht. +/// +/// Ohne `.small`-Familie fällt CarPlay (wie die Smart Stack der Uhr) auf +/// `compactLeading`/`compactTrailing` der Dynamic Island zurück – dort steht +/// nur das generische Symbol und, weil `compactTrailing` leer bleibt, gar +/// kein Text. Erst die `.small`-Familie liefert CarPlay Libelle und Text. +private struct LevelActivityContent: View { + let attributes: LevelActivityAttributes + let state: LevelActivityAttributes.ContentState + + @Environment(\.activityFamily) private var activityFamily + + var body: some View { + switch activityFamily { + case .small: + CompactLevelView(attributes: attributes, state: state) + default: + LockScreenLevelView(attributes: attributes, state: state) + } + } +} + +/// Kompaktform für CarPlay-Dashboard und Smart Stack der Uhr: Libelle plus +/// eine Textzeile, ohne die längere Zwei-Zeilen-Aufschlüsselung des +/// Sperrbildschirms. +private struct CompactLevelView: View { + let attributes: LevelActivityAttributes + let state: LevelActivityAttributes.ContentState + + var body: some View { + HStack(spacing: 12) { + LevelBubbleGlyph(pitch: state.pitch, roll: state.roll, isLevel: state.isLevel, diameter: 40) + + Text(state.instruction + ?? (state.pitch == nil && state.roll == nil ? "Keine Messwerte" : attributes.deviceName)) + .font(.callout.weight(.semibold)) + .foregroundStyle(state.isLevel ? .green : .white) + .lineLimit(1) + .minimumScaleFactor(0.8) + + Spacer(minLength: 0) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .foregroundStyle(.white) + } +} + +/// Sperrbildschirm- und Banner-Ansicht. private struct LockScreenLevelView: View { let attributes: LevelActivityAttributes let state: LevelActivityAttributes.ContentState