Ausrichtungs-Assistent fürs Rangieren

Begleitet das Einparken: verfolgt die Neigung über die Zeit, meldet ob es
besser oder schlechter wird, und erinnert an den flachsten Punkt - "vor 4
Sekunden stand das Fahrzeug 0,8 Grad flacher". Bei Erreichen der Toleranz
gibt es eine Vibration, das Display bleibt währenddessen wach.

Bewusst ohne Positionsbestimmung. Aus einem MEMS-Sensor lässt sich keine
brauchbare Strecke ableiten: der Fehler wächst beim zweifachen Integrieren
quadratisch mit der Zeit, und im Schritttempo gehen die tatsächlichen
Beschleunigungen im Rauschen unter. Gebraucht wird sie auch nicht - beim
Einparken lautet die Frage nie "wo stehe ich", sondern "wird es besser".
Das steckt vollständig im zeitlichen Verlauf der Neigung, ohne jede
Annahme über das Gelände. Die vorhandene Verlaufsaufzeichnung war mit
einem Punkt alle fünf Sekunden zu grob; der Assistent führt einen eigenen
Ringpuffer, der nur läuft solange die Ansicht offen ist.

Sind Spurweite und Radstand hinterlegt, kommt die nötige Höhe der
Auffahrkeile dazu. Das ist reine Geometrie und damit exakt. Die Masse
stehen im Fahrzeugprofil.

Dabei fiel auf, dass sich vorhandene Fahrzeuge praktisch nicht bearbeiten
liessen: das ging nur über eine Wischgeste, die niemand findet - erst
recht nicht, wenn dort jetzt die Masse einzutragen sind. Die Zeile hat nun
einen sichtbaren Knopf dafür, wie bei den WLAN-Einstellungen: antippen
wählt aus, das Zeichen daneben öffnet die Bearbeitung.

Ausserdem übernimmt der Assistent den anliegenden Messwert beim Öffnen.
Sonst stand dort bis zur nächsten Messung "Warte auf Messwerte", obwohl
längst welche vorlagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
BiasF
2026-08-30 15:52:57 +02:00
co-authored by Claude Opus 5
parent 826a201a36
commit 767fe4e798
9 changed files with 598 additions and 16 deletions
+77 -13
View File
@@ -17,8 +17,9 @@ struct ProfilesView: View {
row(for: profile)
}
} footer: {
Text("Jedes Fahrzeug hat seine eigenen Geräte. Die App liest immer "
+ "nur die des gewählten Fahrzeugs aus.")
Text("Antippen wählt das Fahrzeug aus, das ⓘ öffnet Name, Symbol "
+ "und Maße. Jedes Fahrzeug hat seine eigenen Geräte, und die "
+ "App liest immer nur die des gewählten aus.")
}
Section {
@@ -59,30 +60,42 @@ struct ProfilesView: View {
}
private func row(for profile: Profile) -> some View {
HStack {
HStack(spacing: 12) {
Label {
VStack(alignment: .leading, spacing: 2) {
Text(profile.name)
Text(deviceSummary(for: profile))
Text(summary(for: profile))
.font(.caption)
.foregroundStyle(.secondary)
}
} icon: {
Image(systemName: profile.symbol)
}
// Nur dieser Bereich wählt aus; der Knopf rechts bleibt frei.
.contentShape(.rect)
.onTapGesture {
store.selectProfile(profile)
bluetooth.refreshConfiguration()
}
Spacer()
Spacer(minLength: 0)
if profile.id == store.activeProfileID {
Image(systemName: "checkmark")
.foregroundStyle(.tint)
.fontWeight(.semibold)
}
}
.contentShape(.rect)
.onTapGesture {
store.selectProfile(profile)
bluetooth.refreshConfiguration()
// Zum Bearbeiten reichte bisher nur das Wischen - das findet
// niemand, erst recht nicht für die Fahrzeugmaße.
Button {
editing = profile
} label: {
Image(systemName: "info.circle")
.font(.title3)
}
.buttonStyle(.borderless)
.accessibilityLabel("\(profile.name) bearbeiten")
}
.swipeActions(edge: .trailing) {
// Das letzte Fahrzeug muss bleiben, sonst hätten Geräte keinen Ort.
@@ -98,14 +111,34 @@ struct ProfilesView: View {
}
}
private func deviceSummary(for profile: Profile) -> String {
private func summary(for profile: Profile) -> String {
let count = store.deviceCount(in: profile)
return count == 1 ? "1 Gerät" : "\(count) Geräte"
var parts = [count == 1 ? "1 Gerät" : "\(count) Geräte"]
if profile.trackWidth != nil || profile.wheelbase != nil {
parts.append("Maße hinterlegt")
}
return parts.joined(separator: " · ")
}
}
/// Name und Symbol eines Fahrzeugs ändern.
/// Name, Symbol und Maße eines Fahrzeugs ändern.
private struct ProfileEditView: View {
/// Nimmt Komma wie Punkt an auf einer deutschen Tastatur liegt das Komma
/// näher. Unplausible Werte werden verworfen, damit die Keilhöhe nicht
/// stillschweigend Unsinn ergibt.
static func metres(from text: String) -> Double? {
let cleaned = text.replacingOccurrences(of: ",", with: ".")
.trimmingCharacters(in: .whitespaces)
guard let value = Double(cleaned), value > 0.5, value < 12 else { return nil }
return value
}
static func text(from value: Double?) -> String {
guard let value else { return "" }
return String(format: "%.2f", value).replacingOccurrences(of: ".", with: ",")
}
let profile: Profile
@Environment(DeviceStore.self) private var store
@@ -113,6 +146,8 @@ private struct ProfileEditView: View {
@State private var name = ""
@State private var symbol = "box.truck"
@State private var trackWidth = ""
@State private var wheelbase = ""
private let columns = [GridItem(.adaptive(minimum: 60), spacing: 12)]
@@ -122,6 +157,31 @@ private struct ProfileEditView: View {
Section("Name") {
TextField("Name", text: $name)
}
Section {
LabeledContent("Spurweite") {
HStack {
TextField("z. B. 2,00", text: $trackWidth)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
Text("m").foregroundStyle(.secondary)
}
}
LabeledContent("Radstand") {
HStack {
TextField("z. B. 3,50", text: $wheelbase)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
Text("m").foregroundStyle(.secondary)
}
}
} header: {
Text("Fahrzeugmaße")
} footer: {
Text("Nur für den Ausrichtungs-Assistenten: Aus Neigung und "
+ "Abstand errechnet er, wie hoch der Auffahrkeil sein muss. "
+ "Ohne Angabe entfällt diese Anzeige.")
}
Section("Symbol") {
LazyVGrid(columns: columns, spacing: 12) {
ForEach(Profile.symbols, id: \.self) { candidate in
@@ -153,6 +213,8 @@ private struct ProfileEditView: View {
var updated = profile
updated.name = name.trimmingCharacters(in: .whitespaces)
updated.symbol = symbol
updated.trackWidth = Self.metres(from: trackWidth)
updated.wheelbase = Self.metres(from: wheelbase)
store.update(updated)
dismiss()
}
@@ -162,6 +224,8 @@ private struct ProfileEditView: View {
.onAppear {
name = profile.name
symbol = profile.symbol
trackWidth = Self.text(from: profile.trackWidth)
wheelbase = Self.text(from: profile.wheelbase)
}
}
}