App: Einrueckung haelt auch bei Umbruch, Loeschen fragt mittig

Eingerueckt wurde mit fuehrenden Leerzeichen. Die verschieben aber nur die
ERSTE Zeile: Bricht ein langer Name um ("Schublade Links Unten"), faengt die
Fortsetzung wieder ganz links an und die Hierarchie ist weg.

In einem aufklappenden Menue laesst sich das nicht loesen - dort liefert man
nur Text ab, das System ignoriert jedes Layout (auch lineLimit). Die
Lagerort- und Kategorieauswahl erscheint deshalb jetzt als Liste
(.pickerStyle(.navigationLink)). Dort ist jede Zeile eine echte Ansicht, ein
Innenabstand rueckt den ganzen Block ein - auch die zweite Zeile. Das liest
sich bei vielen Lagerorten ohnehin besser als ein langes Aufklappmenue.

Zwei enge Zeilen bleiben Menues: Mindestbestand je Ort bei Artikel und
Gruppe, wo direkt daneben das Mengenfeld steht. Dort wuerde eine
Listenauswahl die Zeile sprengen; sie behalten die Leerzeichen-Einrueckung.

Der Loeschdialog war ein confirmationDialog und fuhr als Blase von unten
herein. Jetzt ein alert: steht mittig und will gelesen werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-15 21:12:29 +02:00
co-authored by Claude Opus 5
parent ea7a42ac22
commit d7002e2252
7 changed files with 46 additions and 13 deletions
+25 -3
View File
@@ -1,4 +1,4 @@
import Foundation
import SwiftUI
/// Eine Zeile einer flach ausgelegten Baumliste, mit ihrer Tiefe.
///
@@ -12,11 +12,33 @@ struct Baumzeile<T: TreeItem>: Identifiable {
var id: T.ID { item.id }
/// Name mit Einrueckung je Ebene. Macht die Hierarchie in einer flachen
/// Auswahl sichtbar, ohne jede Zeile mit dem vollen Pfad zu ueberladen.
/// Name mit Einrueckung aus fuehrenden Leerzeichen.
///
/// Nur fuer aufklappende Menues, wo man ausser Text nichts abliefern kann.
/// Der Nachteil bleibt dort bestehen: Bricht ein langer Name um, faengt die
/// zweite Zeile ganz links an. Wo es geht, `BaumZeile` nehmen.
var label: String { String(repeating: " ", count: tiefe) + item.name }
}
/// Auswahlzeile mit echter Einrueckung.
///
/// Fuehrende Leerzeichen verschieben nur die erste Zeile - ein langer Name
/// bricht um und die Fortsetzung steht wieder ganz links, womit die Hierarchie
/// verloren geht. Ein Innenabstand rueckt dagegen den ganzen Block ein.
///
/// Verlangt eine Auswahl, die als Liste erscheint (`.pickerStyle(.navigationLink)`):
/// In einem aufklappenden Menue zeigt das System nur den Text und ignoriert
/// jedes Layout.
struct BaumZeile<T: TreeItem>: View {
let zeile: Baumzeile<T>
var body: some View {
Text(zeile.item.name)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, CGFloat(zeile.tiefe) * 16)
}
}
/// Bringt eine flache Eltern-Kind-Liste in Baumreihenfolge, mit Tiefe.
///
/// Waisen - deren Elternteil weggefiltert oder geloescht ist - gelten als
+4 -1
View File
@@ -14,9 +14,12 @@ struct CategoryPicker: View {
Picker("Kategorie", selection: $selection) {
Text("– keine –").tag(Int?.none)
ForEach(baum(gefiltert)) { eintrag in
Text(eintrag.label).tag(Int?.some(eintrag.item.id))
BaumZeile(zeile: eintrag).tag(Int?.some(eintrag.item.id))
}
}
// Als Liste statt als Menue: Nur so rueckt ein umbrechender langer
// Name auch in der zweiten Zeile ein.
.pickerStyle(.navigationLink)
}
private var gefiltert: [CategoryItem] {
+2 -1
View File
@@ -212,9 +212,10 @@ struct CheckInFormView: View {
Picker("Lagerort", selection: $locationId) {
Text("– keiner –").tag(String?.none)
ForEach(baum(locations)) { eintrag in
Text(eintrag.label).tag(String?.some(eintrag.item.id))
BaumZeile(zeile: eintrag).tag(String?.some(eintrag.item.id))
}
}
.pickerStyle(.navigationLink)
Button {
locScanError = nil
locScanPaused = false
+4 -2
View File
@@ -340,8 +340,9 @@ struct ItemEditView: View {
Section {
Picker("Lagerort", selection: $locationId) {
Text("– ohne –").tag(String?.none)
ForEach(baum(locations)) { e in Text(e.label).tag(String?.some(e.item.id)) }
ForEach(baum(locations)) { e in BaumZeile(zeile: e).tag(String?.some(e.item.id)) }
}
.pickerStyle(.navigationLink)
// Lagerort per QR setzen – ohne den (evtl. nicht vorhandenen) QR
// des Einzelstücks. Praktisch z.B. bei Kleidung.
Button { locScanShown = true } label: {
@@ -876,8 +877,9 @@ struct ItemAddSheet: View {
Stepper("Anzahl: \(count)", value: $count, in: 1...200)
Picker("Lagerort", selection: $locationId) {
Text("– ohne –").tag(String?.none)
ForEach(baum(locations)) { e in Text(e.label).tag(String?.some(e.item.id)) }
ForEach(baum(locations)) { e in BaumZeile(zeile: e).tag(String?.some(e.item.id)) }
}
.pickerStyle(.navigationLink)
Button { locScanShown = true } label: {
Label("Lagerort scannen", systemImage: "qrcode.viewfinder")
}
+4 -2
View File
@@ -447,9 +447,10 @@ struct LocationEditor: View {
Picker("Übergeordnet", selection: $parentId) {
Text("– oberste Ebene –").tag(String?.none)
ForEach(baum(parentOptions)) { eintrag in
Text(eintrag.label).tag(String?.some(eintrag.item.id))
BaumZeile(zeile: eintrag).tag(String?.some(eintrag.item.id))
}
}
.pickerStyle(.navigationLink)
} header: {
Text("Übergeordneter Lagerort (optional)")
}
@@ -786,9 +787,10 @@ struct CategoryEditor: View {
Picker("Übergeordnet", selection: $parentId) {
Text("– oberste Ebene –").tag(Int?.none)
ForEach(baum(all)) { eintrag in
Text(eintrag.label).tag(Int?.some(eintrag.item.id))
BaumZeile(zeile: eintrag).tag(Int?.some(eintrag.item.id))
}
}
.pickerStyle(.navigationLink)
} header: {
Text("Übergeordnete Kategorie (optional)")
} footer: {
+2 -1
View File
@@ -152,8 +152,9 @@ private struct LocationPicker: View {
var body: some View {
Picker(title, selection: $selection) {
Text("– ohne Lagerort –").tag(String?.none)
ForEach(baum(locations)) { e in Text(e.label).tag(String?.some(e.item.id)) }
ForEach(baum(locations)) { e in BaumZeile(zeile: e).tag(String?.some(e.item.id)) }
}
.pickerStyle(.navigationLink)
}
}
+5 -3
View File
@@ -349,8 +349,9 @@ struct ProductDetailView: View {
Text("Änderungen an Name/Marke/Kategorie sind noch nicht gespeichert. "
+ "Foto und Lagerort-Bedarf werden sofort gespeichert.")
}
.confirmationDialog("„\(current.name)“ löschen?", isPresented: $pendingDelete,
titleVisibility: .visible) {
// alert und nicht confirmationDialog: Letzteres faehrt als Blase von
// unten herein. Ein Loeschen will mittig stehen und gelesen werden.
.alert("„\(current.name)“ löschen?", isPresented: $pendingDelete) {
Button("Löschen", role: .destructive) { Task { await loeschen() } }
Button("Abbrechen", role: .cancel) {}
} message: {
@@ -725,8 +726,9 @@ struct LotEditView: View {
Section("Lagerort") {
Picker("Lagerort", selection: $locationId) {
Text("– ohne –").tag(String?.none)
ForEach(baum(locations)) { e in Text(e.label).tag(String?.some(e.item.id)) }
ForEach(baum(locations)) { e in BaumZeile(zeile: e).tag(String?.some(e.item.id)) }
}
.pickerStyle(.navigationLink)
// Ort per QR am Regal/Fach setzen, statt in der Liste zu suchen.
Button { locScanShown = true } label: {
Label("Lagerort scannen", systemImage: "qrcode.viewfinder")