From 8aa8658b5cd92faf80c5d1bfb171fc6fade03f16 Mon Sep 17 00:00:00 2001 From: BiasF Date: Mon, 31 Aug 2026 21:59:52 +0200 Subject: [PATCH] Uhr: Nivellierung auf drei Seiten, Assistent passt ohne Scrollen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CamperMonitorWatch/Views/WatchAlignView.swift | 94 ++++++++++--------- CamperMonitorWatch/Views/WatchLevelView.swift | 90 +++++++++++++++--- CamperMonitorWatch/Views/WatchRootView.swift | 2 - 3 files changed, 126 insertions(+), 60 deletions(-) diff --git a/CamperMonitorWatch/Views/WatchAlignView.swift b/CamperMonitorWatch/Views/WatchAlignView.swift index e502146..ddcb94f 100644 --- a/CamperMonitorWatch/Views/WatchAlignView.swift +++ b/CamperMonitorWatch/Views/WatchAlignView.swift @@ -20,52 +20,58 @@ struct WatchAlignView: View { private var state: LevelState? { reading.hasReading ? reading.state : nil } var body: some View { - ScrollView { - VStack(spacing: 8) { - if let current = assistant.current { - Text(String(format: "%.1f°", current.deviation)) - .font(.system(size: 40, weight: .semibold, design: .rounded)) - .monospacedDigit() - .foregroundStyle(assistant.hasReachedTarget ? .green : .primary) - .contentTransition(.numericText()) - } else { - ProgressView() - .padding(.vertical, 12) - } - - Label(assistant.trend.text, systemImage: assistant.trend.symbol) - .font(.footnote.weight(.medium)) - .foregroundStyle(trendColor) - - Text(assistant.advice) - .font(.caption) - .multilineTextAlignment(.center) - .foregroundStyle(.secondary) - - if let state { - HStack(spacing: 12) { - WatchBubble(pitch: state.pitch, roll: state.roll) - .frame(width: 56, height: 56) - VStack(alignment: .leading, spacing: 2) { - Text(String(format: "Längs %.1f°", state.pitch ?? 0)) - Text(String(format: "Quer %.1f°", state.roll ?? 0)) - } - .font(.caption2) - .monospacedDigit() - .foregroundStyle(.secondary) - } - .padding(.top, 2) - } - - Button("Von vorn messen", systemImage: "arrow.counterclockwise") { - assistant.reset() - hasAnnouncedTarget = false - } - .font(.caption2) - .padding(.top, 4) + // 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) } - .padding(.horizontal, 4) + + 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 } diff --git a/CamperMonitorWatch/Views/WatchLevelView.swift b/CamperMonitorWatch/Views/WatchLevelView.swift index 0061ed3..58aa213 100644 --- a/CamperMonitorWatch/Views/WatchLevelView.swift +++ b/CamperMonitorWatch/Views/WatchLevelView.swift @@ -1,16 +1,45 @@ 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 /// wenn der gerade nicht erreichbar ist, tritt der zuletzt über das iPhone -/// gemeldete Stand an seine Stelle. Woher sie stammen, steht unten – ein alter -/// Wert sieht sonst aus wie ein stillstehendes Fahrzeug. +/// gemeldete Stand an seine Stelle. Woher sie stammen, steht auf der ersten +/// Seite – ein alter Wert sieht sonst aus wie ein stillstehendes Fahrzeug. /// /// 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, /// die Nullage aus Versehen zu verstellen. 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(WatchLevelRadio.self) private var radio @AppStorage("levelDisplayStyle") private var showsVehicle = true @@ -36,16 +65,6 @@ struct WatchLevelView: View { .multilineTextAlignment(.center) .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 { showsVehicle.toggle() } label: { @@ -58,7 +77,7 @@ struct WatchLevelView: View { } .padding(.horizontal, 4) } - .navigationTitle("Nivellierung") + .navigationTitle("Neigung") } private func angle(_ title: String, _ value: Double?) -> some View { @@ -71,6 +90,49 @@ struct WatchLevelView: View { .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 /// haben, wenn im Fahrzeugprofil Spurweite und Radstand stehen. diff --git a/CamperMonitorWatch/Views/WatchRootView.swift b/CamperMonitorWatch/Views/WatchRootView.swift index 18b9f6b..3492b4f 100644 --- a/CamperMonitorWatch/Views/WatchRootView.swift +++ b/CamperMonitorWatch/Views/WatchRootView.swift @@ -18,7 +18,6 @@ struct WatchRootView: View { .navigationDestination(for: WatchRoute.self) { route in switch route { case .level: WatchLevelView() - case .align: WatchAlignView() case .device(let id): WatchDeviceDetailView(deviceID: id) } } @@ -81,7 +80,6 @@ struct WatchRootView: View { /// gesetzt wird – von der Komplikation. enum WatchRoute: Hashable { case level - case align case device(UUID) }