forked from fritob/Camper-Monitor
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>
79 lines
3.1 KiB
Swift
79 lines
3.1 KiB
Swift
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)
|
||
}
|
||
}
|