From 21f5d8de6207449df04be92415b9c43174a3e790 Mon Sep 17 00:00:00 2001 From: fototeddy Date: Sat, 5 Sep 2026 14:08:40 +0200 Subject: [PATCH] Simplify CarPlay Live Activity to letter+arrow glance indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the full sentence ("Heck steht höher, links steht höher") in the .small activity family with a compact "H ↑ L" style readout: a direction letter per axis (only shown when outside tolerance) around a single up arrow, since only the higher side is ever named anyway. Text now shares the same green/orange/red scale as the bubble glyph (factored into one levelColour helper instead of two duplicated thresholds), and uses a scalable font style with minimumScaleFactor so it fits whatever size CarPlay actually grants the card - there's no API to query the physical display size from Live Activity content. Co-Authored-By: Claude Sonnet 5 --- .../VanAligneiOSWidgetLiveActivity.swift | 89 ++++++++++++++----- 1 file changed, 67 insertions(+), 22 deletions(-) diff --git a/VanAligneiOSWidget/VanAligneiOSWidgetLiveActivity.swift b/VanAligneiOSWidget/VanAligneiOSWidgetLiveActivity.swift index 00d49a7..76b315f 100644 --- a/VanAligneiOSWidget/VanAligneiOSWidgetLiveActivity.swift +++ b/VanAligneiOSWidget/VanAligneiOSWidgetLiveActivity.swift @@ -19,6 +19,20 @@ private struct LevelIcon: View { } } +/// Muss mit `LevelState.levelTolerance` übereinstimmen. +private let levelTolerance: Double = 0.5 + +/// Grün bis Toleranz, orange bis 2°, sonst rot – dieselbe Skala für Blase +/// und Text, wie in der App (`VehicleTilt.colour`). Hier dupliziert, weil +/// die Extension `Shared/` nicht mitkompiliert (siehe +/// `LevelActivityAttributes.swift`). +private func levelColour(forDeviation deviation: Double?) -> Color { + guard let deviation else { return .white.opacity(0.4) } + if deviation <= levelTolerance { return .green } + if deviation <= 2 { return .orange } + return .red +} + /// Verkleinerte Libelle für Sperrbildschirm/CarPlay und die erweiterte /// Dynamic Island – dieselbe Optik wie `LevelBubble` in der App, aber ohne /// deren Abhängigkeit auf `Shared/Models/LevelState.swift`, das in der @@ -31,17 +45,12 @@ private struct LevelBubbleGlyph: View { /// Bis zu welcher Neigung die Blase ausschlägt, wie in der App-Libelle. private let range: Double = 6 - /// Muss mit `LevelState.levelTolerance` übereinstimmen. - private let tolerance: Double = 0.5 private var hasReading: Bool { pitch != nil || roll != nil } private var bubbleColor: Color { guard let pitch, let roll else { return .white.opacity(0.4) } - let deviation = max(abs(pitch), abs(roll)) - if deviation <= tolerance { return .green } - if deviation <= 2 { return .orange } - return .red + return levelColour(forDeviation: max(abs(pitch), abs(roll))) } var body: some View { @@ -50,7 +59,7 @@ private struct LevelBubbleGlyph: View { let travel = radius - bubble / 2 - 3 let offsetX = clamped(roll) / range * travel let offsetY = clamped(pitch) / range * travel - let toleranceRadius = max(tolerance / range * travel, bubble * 0.55) + let toleranceRadius = max(levelTolerance / range * travel, bubble * 0.55) ZStack { Circle() @@ -137,28 +146,67 @@ private struct LevelActivityContent: View { } /// Kompaktform für CarPlay-Dashboard und Smart Stack der Uhr: Libelle plus -/// eine Textzeile, ohne die längere Zwei-Zeilen-Aufschlüsselung des -/// Sperrbildschirms. +/// ein Blick-Symbol statt eines ganzen Satzes – zum Lesen im Vorbeifahren. +/// +/// Gezeigt wird nur, welche Seite höher steht: Buchstabe (Längsneigung), +/// Pfeil, Buchstabe (Querneigung) – z.B. "H ↑ L" für "Heck und links stehen +/// höher". Ein einzelner Pfeil in der Mitte reicht, weil ohnehin nur die +/// höher stehende Seite benannt wird ("höher" ist implizit "nach oben"). private struct CompactLevelView: View { let attributes: LevelActivityAttributes let state: LevelActivityAttributes.ContentState + private var hasReading: Bool { state.pitch != nil || state.roll != nil } + + /// "H"/"F", nur wenn ausserhalb der Toleranz. + private var pitchLetter: String? { + guard let pitch = state.pitch, abs(pitch) > levelTolerance else { return nil } + return pitch >= 0 ? "H" : "F" + } + + /// "R"/"L", nur wenn ausserhalb der Toleranz. + private var rollLetter: String? { + guard let roll = state.roll, abs(roll) > levelTolerance else { return nil } + return roll >= 0 ? "R" : "L" + } + + /// Dieselbe Skala wie die Blase, über die grössere der beiden Neigungen. + private var statusColor: Color { + guard let pitch = state.pitch, let roll = state.roll else { return .white.opacity(0.4) } + return levelColour(forDeviation: max(abs(pitch), abs(roll))) + } + var body: some View { - HStack(spacing: 12) { + HStack(spacing: 14) { 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) + Group { + if !hasReading { + Text("–") + } else if state.isLevel { + Image(systemName: "checkmark.circle.fill") + } else { + HStack(spacing: 6) { + if let pitchLetter { Text(pitchLetter) } + Image(systemName: "arrow.up") + if let rollLetter { Text(rollLetter) } + } + } + } + // Skalierbarer Textstil statt fester Punktgrösse: CarPlay- + // Bildschirme unterscheiden sich in Grösse, die exakten Masse + // eines Displays sind aber nicht abfragbar. `minimumScaleFactor` + // lässt die Schrift so gross wie möglich, aber so klein wie + // nötig werden, um in den vom System zugeteilten Platz zu passen. + .font(.title2.weight(.bold)) + .minimumScaleFactor(0.5) + .lineLimit(1) + .foregroundStyle(statusColor) Spacer(minLength: 0) } .padding(.horizontal, 12) .padding(.vertical, 8) - .foregroundStyle(.white) } } @@ -167,20 +215,17 @@ private struct LockScreenLevelView: View { let attributes: LevelActivityAttributes let state: LevelActivityAttributes.ContentState - /// Muss mit `LevelState.levelTolerance` übereinstimmen. - private let tolerance: Double = 0.5 - /// Längszeile, z.B. "Heck steht höher 1.8°" – nur wenn ausserhalb der /// Toleranz, genau wie bei `LevelState.instruction` in der App. private var pitchLine: String? { - guard let pitch = state.pitch, abs(pitch) > tolerance else { return nil } + guard let pitch = state.pitch, abs(pitch) > levelTolerance else { return nil } let label = pitch >= 0 ? "Heck steht höher" : "Front steht höher" return "\(label) \(LevelDirectionFormatting.magnitude(pitch))" } /// Querzeile, z.B. "links steht höher 0.9°". private var rollLine: String? { - guard let roll = state.roll, abs(roll) > tolerance else { return nil } + guard let roll = state.roll, abs(roll) > levelTolerance else { return nil } let label = roll >= 0 ? "rechts steht höher" : "links steht höher" return "\(label) \(LevelDirectionFormatting.magnitude(roll))" }