import SwiftUI /// Fahrzeuge anlegen, umbenennen und entfernen. struct ProfilesView: View { @Environment(DeviceStore.self) private var store @Environment(BluetoothManager.self) private var bluetooth @Environment(\.dismiss) private var dismiss @State private var editing: Profile? @State private var pendingDeletion: Profile? var body: some View { NavigationStack { List { Section { ForEach(store.profiles) { profile in row(for: profile) } } footer: { 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 { Button("Fahrzeug hinzufügen", systemImage: "plus") { let profile = store.addProfile(named: "Camper \(store.profiles.count + 1)") editing = profile } } } .navigationTitle("Fahrzeuge") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Fertig") { dismiss() } } } .sheet(item: $editing) { profile in ProfileEditView(profile: profile) } .confirmationDialog("Fahrzeug entfernen?", isPresented: .init(get: { pendingDeletion != nil }, set: { if !$0 { pendingDeletion = nil } }), titleVisibility: .visible) { Button("Entfernen", role: .destructive) { if let pendingDeletion { store.removeProfile(pendingDeletion) bluetooth.refreshConfiguration() } pendingDeletion = nil } } message: { if let pendingDeletion { Text("„\(pendingDeletion.name)“ und die \(store.deviceCount(in: pendingDeletion)) " + "darin eingerichteten Geräte werden gelöscht.") } } } } private func row(for profile: Profile) -> some View { HStack(spacing: 12) { Label { VStack(alignment: .leading, spacing: 2) { Text(profile.name) 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(minLength: 0) if profile.id == store.activeProfileID { Image(systemName: "checkmark") .foregroundStyle(.tint) .fontWeight(.semibold) } // 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. if store.hasMultipleProfiles { Button("Entfernen", systemImage: "trash", role: .destructive) { pendingDeletion = profile } } Button("Bearbeiten", systemImage: "pencil") { editing = profile } .tint(.gray) } } private func summary(for profile: Profile) -> String { let count = store.deviceCount(in: profile) 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, 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 @Environment(\.dismiss) private var dismiss @State private var name = "" @State private var symbol = "suv.side" @State private var trackWidth = "" @State private var wheelbase = "" private let columns = [GridItem(.adaptive(minimum: 60), spacing: 12)] var body: some View { NavigationStack { Form { 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 Button { symbol = candidate } label: { Image(systemName: candidate) .font(.title2) .frame(width: 52, height: 52) .background(symbol == candidate ? Color.accentColor.opacity(0.18) : Color.secondary.opacity(0.08), in: .rect(cornerRadius: 12)) .foregroundStyle(symbol == candidate ? Color.accentColor : Color.primary) } .buttonStyle(.plain) } } .padding(.vertical, 4) } } .navigationTitle("Fahrzeug") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Abbrechen") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Sichern") { 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() } .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty) } } .onAppear { name = profile.name symbol = profile.symbol trackWidth = Self.text(from: profile.trackWidth) wheelbase = Self.text(from: profile.wheelbase) } } } }