Uhr: Nivellierung auf drei Seiten, Assistent passt ohne Scrollen

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>
This commit is contained in:
BiasF
2026-08-31 21:59:52 +02:00
co-authored by Claude Opus 5
parent 82df240ebc
commit 8aa8658b5c
3 changed files with 126 additions and 60 deletions
+22 -16
View File
@@ -20,52 +20,58 @@ struct WatchAlignView: View {
private var state: LevelState? { reading.hasReading ? reading.state : nil } private var state: LevelState? { reading.hasReading ? reading.state : nil }
var body: some View { var body: some View {
ScrollView { // Ohne ScrollView: Beim Rangieren schaut man kurz hin, da darf nichts
VStack(spacing: 8) { // 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 { if let current = assistant.current {
Text(String(format: "%.1f°", current.deviation)) Text(String(format: "%.1f°", current.deviation))
.font(.system(size: 40, weight: .semibold, design: .rounded)) .font(.system(size: 30, weight: .semibold, design: .rounded))
.monospacedDigit() .monospacedDigit()
.foregroundStyle(assistant.hasReachedTarget ? .green : .primary) .foregroundStyle(assistant.hasReachedTarget ? .green : .primary)
.contentTransition(.numericText()) .contentTransition(.numericText())
} else { } else {
ProgressView() ProgressView()
.padding(.vertical, 12) .padding(.vertical, 6)
} }
Label(assistant.trend.text, systemImage: assistant.trend.symbol) Label(assistant.trend.text, systemImage: assistant.trend.symbol)
.font(.footnote.weight(.medium)) .font(.caption2.weight(.medium))
.foregroundStyle(trendColor) .foregroundStyle(trendColor)
Text(assistant.advice) Text(assistant.advice)
.font(.caption) .font(.system(size: 11))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.lineLimit(2)
.minimumScaleFactor(0.8)
if let state { if let state {
HStack(spacing: 12) { HStack(spacing: 8) {
WatchBubble(pitch: state.pitch, roll: state.roll) WatchBubble(pitch: state.pitch, roll: state.roll)
.frame(width: 56, height: 56) .frame(width: 40, height: 40)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 1) {
Text(String(format: "Längs %.1f°", state.pitch ?? 0)) Text(String(format: "Längs %.1f°", state.pitch ?? 0))
Text(String(format: "Quer %.1f°", state.roll ?? 0)) Text(String(format: "Quer %.1f°", state.roll ?? 0))
} }
.font(.caption2) .font(.system(size: 11))
.monospacedDigit() .monospacedDigit()
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
.padding(.top, 2)
} }
Button("Von vorn messen", systemImage: "arrow.counterclockwise") { Button {
assistant.reset() assistant.reset()
hasAnnouncedTarget = false hasAnnouncedTarget = false
} label: {
Label("Von vorn", systemImage: "arrow.counterclockwise")
.font(.system(size: 10))
} }
.font(.caption2) .buttonStyle(.borderless)
.padding(.top, 4) .foregroundStyle(.secondary)
}
.padding(.horizontal, 4)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.horizontal, 2)
.navigationTitle("Ausrichten") .navigationTitle("Ausrichten")
.onChange(of: tilt, initial: true) { _, tilt in .onChange(of: tilt, initial: true) { _, tilt in
guard let tilt else { return } guard let tilt else { return }
+76 -14
View File
@@ -1,16 +1,45 @@
import SwiftUI import SwiftUI
/// Nivellierung auf der Uhr. /// Nivellierung auf der Uhr, aufgeteilt auf drei Seiten zum Wischen.
///
/// Auf einem Bildschirm dieser Grösse ist Wischen besser als Scrollen: Beim
/// Rangieren trifft man einen Wisch auch ohne hinzusehen, eine Scrollposition
/// dagegen nicht und was gerade wichtig ist, hängt vom Arbeitsschritt ab.
///
/// 1. **Neigung** wie das Fahrzeug steht, als Fahrzeugansicht oder Libelle.
/// 2. **Keile** wie hoch jedes Rad unterlegt werden muss.
/// 3. **Ausrichten** der Assistent fürs Rangieren, mit Vibration.
/// ///
/// Die Werte kommen bevorzugt **direkt vom Sensor** (`WatchLevelRadio`); nur /// Die Werte kommen bevorzugt **direkt vom Sensor** (`WatchLevelRadio`); nur
/// wenn der gerade nicht erreichbar ist, tritt der zuletzt über das iPhone /// wenn der gerade nicht erreichbar ist, tritt der zuletzt über das iPhone
/// gemeldete Stand an seine Stelle. Woher sie stammen, steht unten ein alter /// gemeldete Stand an seine Stelle. Woher sie stammen, steht auf der ersten
/// Wert sieht sonst aus wie ein stillstehendes Fahrzeug. /// Seite ein alter Wert sieht sonst aus wie ein stillstehendes Fahrzeug.
/// ///
/// Kalibriert wird hier nichts. Das gehört einmalig ans iPhone, mit ebenem /// Kalibriert wird hier nichts. Das gehört einmalig ans iPhone, mit ebenem
/// Fahrzeug und Ruhe dafür; auf der Uhr wäre der Knopf nur eine Gelegenheit, /// Fahrzeug und Ruhe dafür; auf der Uhr wäre der Knopf nur eine Gelegenheit,
/// die Nullage aus Versehen zu verstellen. /// die Nullage aus Versehen zu verstellen.
struct WatchLevelView: View { struct WatchLevelView: View {
@State private var page = Page.reading
enum Page: Hashable {
case reading, wedges, align
}
var body: some View {
TabView(selection: $page) {
LevelReadingPage()
.tag(Page.reading)
WedgePage()
.tag(Page.wedges)
WatchAlignView()
.tag(Page.align)
}
.tabViewStyle(.page)
}
}
/// Erste Seite: wie das Fahrzeug steht.
struct LevelReadingPage: View {
@Environment(PhoneLink.self) private var link @Environment(PhoneLink.self) private var link
@Environment(WatchLevelRadio.self) private var radio @Environment(WatchLevelRadio.self) private var radio
@AppStorage("levelDisplayStyle") private var showsVehicle = true @AppStorage("levelDisplayStyle") private var showsVehicle = true
@@ -36,16 +65,6 @@ struct WatchLevelView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundStyle(reading.state.isLevel ? .green : .primary) .foregroundStyle(reading.state.isLevel ? .green : .primary)
if let lift, !lift.isNegligible {
WheelLiftPlan(lift: lift, isCompact: true)
.padding(.top, 2)
}
NavigationLink(value: WatchRoute.align) {
Label("Ausrichten", systemImage: "arrow.left.and.right")
}
.buttonStyle(.borderedProminent)
Button { Button {
showsVehicle.toggle() showsVehicle.toggle()
} label: { } label: {
@@ -58,7 +77,7 @@ struct WatchLevelView: View {
} }
.padding(.horizontal, 4) .padding(.horizontal, 4)
} }
.navigationTitle("Nivellierung") .navigationTitle("Neigung")
} }
private func angle(_ title: String, _ value: Double?) -> some View { private func angle(_ title: String, _ value: Double?) -> some View {
@@ -71,6 +90,49 @@ struct WatchLevelView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
}
/// Zweite Seite: die Keilhöhen, als Draufsicht auf die vier Räder.
struct WedgePage: View {
@Environment(PhoneLink.self) private var link
@Environment(WatchLevelRadio.self) private var radio
private var reading: WatchLevelReading { WatchLevelReading(radio: radio, phone: link) }
var body: some View {
ScrollView {
VStack(spacing: 8) {
if let lift, !lift.isNegligible {
WheelLiftPlan(lift: lift, isCompact: true)
Text("Zentimeter unter das jeweilige Rad. Das höchste bleibt liegen.")
.font(.system(size: 10))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
} else if hasMeasurements {
Label("Keine Keile nötig", systemImage: "checkmark.circle.fill")
.font(.footnote)
.foregroundStyle(.green)
.padding(.top, 20)
} else {
// Ohne Fahrzeugmasse lässt sich nichts rechnen. Das gehört
// gesagt, sonst wirkt die Seite kaputt.
Label("Fahrzeugmasse fehlen", systemImage: "ruler")
.font(.footnote.weight(.medium))
Text("Spurweite und Radstand am iPhone im Fahrzeugprofil "
+ "hinterlegen, dann steht hier die Keilhöhe je Rad.")
.font(.system(size: 10))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
}
.padding(.horizontal, 4)
}
.navigationTitle("Keile")
}
private var hasMeasurements: Bool {
link.payload?.profile.trackWidth != nil && link.payload?.profile.wheelbase != nil
}
/// Wie hoch jedes Rad unterlegt werden muss reine Geometrie, und nur zu /// Wie hoch jedes Rad unterlegt werden muss reine Geometrie, und nur zu
/// haben, wenn im Fahrzeugprofil Spurweite und Radstand stehen. /// haben, wenn im Fahrzeugprofil Spurweite und Radstand stehen.
@@ -18,7 +18,6 @@ struct WatchRootView: View {
.navigationDestination(for: WatchRoute.self) { route in .navigationDestination(for: WatchRoute.self) { route in
switch route { switch route {
case .level: WatchLevelView() case .level: WatchLevelView()
case .align: WatchAlignView()
case .device(let id): WatchDeviceDetailView(deviceID: id) case .device(let id): WatchDeviceDetailView(deviceID: id)
} }
} }
@@ -81,7 +80,6 @@ struct WatchRootView: View {
/// gesetzt wird von der Komplikation. /// gesetzt wird von der Komplikation.
enum WatchRoute: Hashable { enum WatchRoute: Hashable {
case level case level
case align
case device(UUID) case device(UUID)
} }