Alles auf einem Bildschirm hiess scrollen, und auf dieser Grösse ist das der falsche Handgriff: Einen Wisch trifft man beim Rangieren auch ohne hinzusehen, eine Scrollposition nicht. Was gerade zählt, hängt ausserdem vom Arbeitsschritt ab. Jetzt drei Seiten zum Wischen: * Neigung – Fahrzeugansicht oder Libelle, was zu tun ist, Herkunft der Werte * Keile – die Höhe je Rad. Fehlen Spurweite und Radstand, steht das dort, statt dass die Seite leer wirkt. * Ausrichten – der Assistent, vorher hinter einem Knopf Der Assistent ist dabei enger gesetzt: kleinere Zahl, zwei Zeilen Rat mit Skalierung, kleinere Libelle, und kein ScrollView mehr. Beim Rangieren schaut man kurz hin, da darf nichts ausserhalb des Bildes liegen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
4.5 KiB
Swift
116 lines
4.5 KiB
Swift
import SwiftUI
|
||
import WatchKit
|
||
|
||
/// Der Ausrichtungs-Assistent am Handgelenk – der eigentliche Grund für die
|
||
/// Uhr: Beim Rangieren hat man das iPhone nicht in der Hand, aber den Arm am
|
||
/// Lenkrad. Ein Blick nach unten genügt, und die Rückmeldung „steht eben“ kommt
|
||
/// zusätzlich als Vibration, ganz ohne Blick.
|
||
///
|
||
/// Gerechnet wird mit demselben `AlignmentAssistant` wie am iPhone: kein
|
||
/// Ortsbezug, nur der zeitliche Verlauf der Neigung. Was besser und was
|
||
/// schlechter wird, steckt vollständig darin.
|
||
struct WatchAlignView: View {
|
||
@Environment(PhoneLink.self) private var link
|
||
@Environment(WatchLevelRadio.self) private var radio
|
||
@State private var assistant = AlignmentAssistant()
|
||
/// Damit die Vibration einmal kommt und nicht im Sekundentakt.
|
||
@State private var hasAnnouncedTarget = false
|
||
|
||
private var reading: WatchLevelReading { WatchLevelReading(radio: radio, phone: link) }
|
||
private var state: LevelState? { reading.hasReading ? reading.state : nil }
|
||
|
||
var body: some View {
|
||
// Ohne ScrollView: Beim Rangieren schaut man kurz hin, da darf nichts
|
||
// ausserhalb des Bildes liegen. Alles ist deshalb so bemessen, dass es
|
||
// auch auf der kleinsten Uhr auf einen Blick passt.
|
||
VStack(spacing: 3) {
|
||
if let current = assistant.current {
|
||
Text(String(format: "%.1f°", current.deviation))
|
||
.font(.system(size: 30, weight: .semibold, design: .rounded))
|
||
.monospacedDigit()
|
||
.foregroundStyle(assistant.hasReachedTarget ? .green : .primary)
|
||
.contentTransition(.numericText())
|
||
} else {
|
||
ProgressView()
|
||
.padding(.vertical, 6)
|
||
}
|
||
|
||
Label(assistant.trend.text, systemImage: assistant.trend.symbol)
|
||
.font(.caption2.weight(.medium))
|
||
.foregroundStyle(trendColor)
|
||
|
||
Text(assistant.advice)
|
||
.font(.system(size: 11))
|
||
.multilineTextAlignment(.center)
|
||
.foregroundStyle(.secondary)
|
||
.lineLimit(2)
|
||
.minimumScaleFactor(0.8)
|
||
|
||
if let state {
|
||
HStack(spacing: 8) {
|
||
WatchBubble(pitch: state.pitch, roll: state.roll)
|
||
.frame(width: 40, height: 40)
|
||
VStack(alignment: .leading, spacing: 1) {
|
||
Text(String(format: "Längs %.1f°", state.pitch ?? 0))
|
||
Text(String(format: "Quer %.1f°", state.roll ?? 0))
|
||
}
|
||
.font(.system(size: 11))
|
||
.monospacedDigit()
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
|
||
Button {
|
||
assistant.reset()
|
||
hasAnnouncedTarget = false
|
||
} label: {
|
||
Label("Von vorn", systemImage: "arrow.counterclockwise")
|
||
.font(.system(size: 10))
|
||
}
|
||
.buttonStyle(.borderless)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
.padding(.horizontal, 2)
|
||
.navigationTitle("Ausrichten")
|
||
.onChange(of: tilt, initial: true) { _, tilt in
|
||
guard let tilt else { return }
|
||
assistant.add(pitch: tilt.pitch, roll: tilt.roll)
|
||
announceIfLevel()
|
||
}
|
||
}
|
||
|
||
/// Ein Wertepaar als Auslöser: gerechnet wird nur, wenn sich die Neigung
|
||
/// wirklich ändert – nicht bei jeder Aktualisierung der Anzeige.
|
||
private var tilt: Tilt? {
|
||
guard let state, let pitch = state.pitch, let roll = state.roll else { return nil }
|
||
return Tilt(pitch: pitch, roll: roll)
|
||
}
|
||
|
||
private struct Tilt: Equatable {
|
||
let pitch: Double
|
||
let roll: Double
|
||
}
|
||
|
||
private var trendColor: Color {
|
||
switch assistant.trend {
|
||
case .improving: return .green
|
||
case .worsening: return .orange
|
||
default: return .secondary
|
||
}
|
||
}
|
||
|
||
/// Die Vibration ist der Punkt: Sie sagt „anhalten“, ohne dass jemand
|
||
/// hinschauen muss. Erneut gemeldet wird erst, wenn das Fahrzeug die
|
||
/// Toleranz zwischendurch wieder verlassen hat.
|
||
private func announceIfLevel() {
|
||
if assistant.hasReachedTarget {
|
||
guard !hasAnnouncedTarget else { return }
|
||
hasAnnouncedTarget = true
|
||
WKInterfaceDevice.current().play(.success)
|
||
} else {
|
||
hasAnnouncedTarget = false
|
||
}
|
||
}
|
||
}
|