Files
Camper-Monitor/VanControlWatch/Views/WatchBubble.swift
fototeddyandClaude Opus 5 b1ffbe324a Libelle: Blase wandert zur höheren Seite, einheitlich auf allen Anzeigen
Die drei Libellen widersprachen sich: Die Live Activity liess die Blase bei
positivem Pitch nach unten wandern – mit dem Kommentar "wie bei der echten
Wasserwaage in der App" –, waehrend iPhone und Uhr genau das Gegenteil taten.
Gemeint war immer eine Draufsicht mit Fahrtrichtung oben, in der die Blase zur
hoeheren Seite ausschlaegt: Steht das Heck hoeher, gehoert sie nach hinten,
also nach unten. iPhone und Uhr ziehen jetzt nach.

Die Android-Fassung bleibt vorerst bei der alten Richtung.

Dazu `shortInstructions`: dieselbe Aussage wie `instruction`, aber in kurzen
Zeilen statt einem Satz. Auf schmalen Anzeigen passt "Heck steht hoeher, links
steht hoeher" in keine lesbare Zeile. Beide stehen nebeneinander, damit die
Vorzeichen nicht auseinanderlaufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 18:35:14 +02:00

79 lines
3.1 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
/// Libelle für die Uhr: eine Blase, die dorthin wandert, wo das Fahrzeug
/// **höher** steht wie bei einer echten Wasserwaage.
///
/// Bewusst schlichter als am iPhone: Auf 40 mm Bildschirm ist der innere Ring
/// (die Toleranz) die einzige Hilfslinie, die sich noch ablesen lässt.
struct WatchBubble: View {
let pitch: Double?
let roll: Double?
/// Bis zu welcher Neigung die Anzeige ausschlägt.
var range: Double = 4
var body: some View {
GeometryReader { geometry in
let side = min(geometry.size.width, geometry.size.height)
let radius = side / 2
let bubble = side * 0.22
let travel = radius - bubble / 2 - 2
let tolerance = max(LevelState.levelTolerance / range * travel, bubble * 0.55)
ZStack {
Circle().fill(Color.gray.opacity(0.25))
Circle().strokeBorder(Color.gray.opacity(0.5), lineWidth: 1)
Circle()
.strokeBorder(isLevel ? Color.green : Color.gray.opacity(0.6),
lineWidth: isLevel ? 2 : 1)
.frame(width: tolerance * 2, height: tolerance * 2)
Circle()
.fill(color)
.frame(width: bubble, height: bubble)
// Draufsicht mit Fahrtrichtung oben: Positiver Pitch heisst,
// das Heck steht höher die Blase wandert nach hinten, also
// nach unten.
.offset(x: clamped(roll) / range * travel,
y: clamped(pitch) / range * travel)
.animation(.spring(duration: 0.3), value: pitch)
.animation(.spring(duration: 0.3), value: roll)
.opacity(pitch == nil && roll == nil ? 0.25 : 1)
}
.frame(width: side, height: side)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.aspectRatio(1, contentMode: .fit)
.accessibilityLabel(accessibilityText)
}
/// Hält die Blase im Ring, auch wenn das Fahrzeug stark schräg steht.
private func clamped(_ value: Double?) -> Double {
guard let value else { return 0 }
return min(max(value, -range), range)
}
private var deviation: Double? {
switch (pitch, roll) {
case let (p?, r?): return max(abs(p), abs(r))
case let (p?, nil): return abs(p)
case let (nil, r?): return abs(r)
default: return nil
}
}
private var isLevel: Bool { (deviation ?? .infinity) <= LevelState.levelTolerance }
/// Grün nur, wenn es wirklich eben ist die Akzentfarbe der App ist selbst
/// grün, sonst sähe schief aus wie eben.
private var color: Color {
guard let deviation else { return .gray }
if deviation <= LevelState.levelTolerance { return .green }
return deviation <= 2 ? .orange : .red
}
private var accessibilityText: String {
guard let pitch, let roll else { return "Keine Messwerte" }
return String(format: "Längsneigung %.1f Grad, Querneigung %.1f Grad", pitch, roll)
}
}