From e5827280404f98477625e4de230a49f576eb4aab Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 17 Sep 2026 16:22:26 +0200 Subject: [PATCH] =?UTF-8?q?Zebra-Streifen=20f=C3=BCr=20Tabellenansichten?= =?UTF-8?q?=20(found.md=20#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TableZebra-Helper (AppKit NSColor.alternatingContentBackgroundColors) angewendet auf: LAN-Scanner-Geräte/Rohfelder/Port-Scan, Experte- Eintragsliste + "Weitere Parameter"-Grid, Bekannte Router + Interface- Liste (Verbinden), Sicherungsliste, Review/Apply-Log, Übersicht- Knotendetails. Root Cause für "keine Änderung erkennbar": .formStyle(.grouped) malt einen eigenen blickdichten Hintergrund über Section-Zeilen, der .listRowBackground verdeckt — .scrollContentBackground(.hidden) ergänzt. Live bestätigt ("passt"). Co-Authored-By: Claude Sonnet 5 --- .../Core/Models/AppPreferences.swift | 14 ++++++++++++++ .../Features/Backup/BackupListView.swift | 4 +++- .../Features/Devices/DevicesView.swift | 14 ++++++++++++-- .../Features/Expert/ExpertMenuDetailView.swift | 9 +++++++-- .../Features/Overview/OverviewView.swift | 9 +++++++-- .../Wizard/Steps/Connect/ConnectView.swift | 5 ++++- .../Wizard/Steps/Setup/ReviewApplyView.swift | 7 +++++-- found.md | 16 +++++++++++++++- 8 files changed, 67 insertions(+), 11 deletions(-) diff --git a/RouterOSAssistant/Core/Models/AppPreferences.swift b/RouterOSAssistant/Core/Models/AppPreferences.swift index d2048a2..fab2e90 100644 --- a/RouterOSAssistant/Core/Models/AppPreferences.swift +++ b/RouterOSAssistant/Core/Models/AppPreferences.swift @@ -1,4 +1,18 @@ import SwiftUI +import AppKit + +/// Alternating row backgrounds for table-like `Form`/`Section`/`ForEach` lists (LAN-Scanner +/// device rows, Experte-tab item lists) — per explicit readability request. Uses AppKit's own +/// `NSColor.alternatingContentBackgroundColors` (the same pair `NSTableView` uses for zebra +/// striping) rather than a hand-picked gray, so it already matches the system's row-striping +/// color and adapts to Light/Dark Mode for free. +enum TableZebra { + static func color(for index: Int) -> Color { + let pair = NSColor.alternatingContentBackgroundColors + guard pair.count > 1 else { return .clear } + return Color(nsColor: pair[index % 2]) + } +} /// Central catalog for the "Settings" window (⌘,) — every value here is read/written through /// `@AppStorage`/`UserDefaults.standard` under the raw string keys named on each property below, diff --git a/RouterOSAssistant/Features/Backup/BackupListView.swift b/RouterOSAssistant/Features/Backup/BackupListView.swift index 3b5076a..77cbf22 100644 --- a/RouterOSAssistant/Features/Backup/BackupListView.swift +++ b/RouterOSAssistant/Features/Backup/BackupListView.swift @@ -95,7 +95,7 @@ struct BackupListView: View { } else { Form { Section(L10n.t("Sicherungen", appLanguage)) { - ForEach(viewModel.backups) { backup in + ForEach(Array(viewModel.backups.enumerated()), id: \.element.id) { index, backup in HStack { VStack(alignment: .leading) { Text(backup.host).bold() @@ -116,10 +116,12 @@ struct BackupListView: View { .disabled(connectionService.credentials == nil || viewModel.isRestoring) .help(L10n.t("Diese Sicherung auf den verbundenen Router zurückspielen — nur für exakt dasselbe Routermodell.", appLanguage)) } + .listRowBackground(TableZebra.color(for: index)) } } } .formStyle(.grouped) + .scrollContentBackground(.hidden) } } .safeAreaInset(edge: .bottom) { diff --git a/RouterOSAssistant/Features/Devices/DevicesView.swift b/RouterOSAssistant/Features/Devices/DevicesView.swift index 83681db..ab7e0c3 100644 --- a/RouterOSAssistant/Features/Devices/DevicesView.swift +++ b/RouterOSAssistant/Features/Devices/DevicesView.swift @@ -59,7 +59,7 @@ struct DevicesView: View { Text(L10n.t("Keine Geräte", appLanguage)).appFont(.caption).foregroundStyle(.secondary) } else { DeviceColumnHeader(appLanguage: appLanguage) - ForEach(group.devices) { device in + ForEach(Array(group.devices.enumerated()), id: \.element.id) { index, device in HStack(spacing: 4) { DeviceRow(device: device, appLanguage: appLanguage, theme: colorTheme) Spacer() @@ -78,6 +78,7 @@ struct DevicesView: View { .fixedSize() .help(L10n.t("Aktionen", appLanguage)) } + .listRowBackground(TableZebra.color(for: index)) } } } header: { @@ -98,6 +99,12 @@ struct DevicesView: View { } } .formStyle(.grouped) + // `.formStyle(.grouped)` paints its own opaque background over each + // Section's rows — `.listRowBackground` (Zebra-Streifen below) was applied + // correctly but rendered invisible underneath it. Confirmed live ("keine + // Änderungen erkennbar"): this is the documented fix, not a bug in the color + // computation itself. + .scrollContentBackground(.hidden) } } .overlay(alignment: .bottom) { @@ -480,12 +487,14 @@ private struct RawFieldsSheet: View { .foregroundStyle(.secondary) ScrollView { VStack(alignment: .leading, spacing: 4) { - ForEach(device.rawFields, id: \.key) { pair in + ForEach(Array(device.rawFields.enumerated()), id: \.element.key) { index, pair in HStack(alignment: .top) { Text(pair.key).appFont(.caption, design: .monospaced).foregroundStyle(.secondary) Spacer() Text(pair.value).appFont(.caption, design: .monospaced).multilineTextAlignment(.trailing) } + .padding(.vertical, 2) + .background(TableZebra.color(for: index)) } } } @@ -577,6 +586,7 @@ private struct PortScanResultSheet: View { ForEach(Array(result.entries.enumerated()), id: \.element.id) { index, entry in if index > 0 { Divider() } PortScanRow(entry: entry, appLanguage: appLanguage, theme: theme) + .background(TableZebra.color(for: index)) } } } diff --git a/RouterOSAssistant/Features/Expert/ExpertMenuDetailView.swift b/RouterOSAssistant/Features/Expert/ExpertMenuDetailView.swift index 2d4f1bd..39413c7 100644 --- a/RouterOSAssistant/Features/Expert/ExpertMenuDetailView.swift +++ b/RouterOSAssistant/Features/Expert/ExpertMenuDetailView.swift @@ -39,7 +39,7 @@ struct ExpertMenuDetailView: View { // `.sheet(item:)` dismissal already forces a remount of this view on its own; // delete's confirmationDialog doesn't tear this view down at all, so nothing // was forcing SwiftUI to re-diff the Section in place. - ForEach(viewModel.items) { item in + ForEach(Array(viewModel.items.enumerated()), id: \.element.id) { index, item in HStack { Button { viewModel.startEditing(item) @@ -61,6 +61,7 @@ struct ExpertMenuDetailView: View { } } .contentShape(Rectangle()) + .listRowBackground(TableZebra.color(for: index)) } } } header: { @@ -75,6 +76,9 @@ struct ExpertMenuDetailView: View { .id(viewModel.items.count) } .formStyle(.grouped) + // See DevicesView's identical fix: `.formStyle(.grouped)` paints an opaque background + // over each Section's rows, hiding `.listRowBackground` (Zebra-Streifen) underneath it. + .scrollContentBackground(.hidden) .navigationTitle(LocalizedStringKey(L10n.t(schema.displayName, appLanguage))) .task(id: schema.id) { await viewModel.reloadItems() } .sheet(item: $viewModel.editingItem) { item in @@ -171,7 +175,7 @@ struct ExpertItemEditView: View { extraColumnHeader("Parameter") extraColumnHeader("Wert") } - ForEach(Array(stride(from: 0, to: viewModel.extraFields.count, by: 2)), id: \.self) { start in + ForEach(Array(stride(from: 0, to: viewModel.extraFields.count, by: 2).enumerated()), id: \.element) { rowIndex, start in GridRow { extraKeyField(at: start) extraValueField(at: start) @@ -188,6 +192,7 @@ struct ExpertItemEditView: View { Color.clear } } + .background(TableZebra.color(for: rowIndex)) Divider() .gridCellColumns(5) } diff --git a/RouterOSAssistant/Features/Overview/OverviewView.swift b/RouterOSAssistant/Features/Overview/OverviewView.swift index 6ef6448..b0ce232 100644 --- a/RouterOSAssistant/Features/Overview/OverviewView.swift +++ b/RouterOSAssistant/Features/Overview/OverviewView.swift @@ -833,12 +833,14 @@ private struct NodeDetailView: View { if !node.detail.isEmpty { Divider() VStack(alignment: .leading, spacing: 4) { - ForEach(node.detail, id: \.key) { pair in + ForEach(Array(node.detail.enumerated()), id: \.element.key) { index, pair in HStack(alignment: .top) { Text(pair.key).appFont(.caption, design: .monospaced).foregroundStyle(.secondary) Spacer() Text(pair.value).appFont(.caption, design: .monospaced).multilineTextAlignment(.trailing) } + .padding(.vertical, 2) + .background(TableZebra.color(for: index)) } } } @@ -847,7 +849,7 @@ private struct NodeDetailView: View { if !connected.isEmpty { Divider() Text(L10n.t("Verbindungen", appLanguage)).appFont(.subheadline, bold: true) - ForEach(connected) { edge in + ForEach(Array(connected.enumerated()), id: \.element.id) { index, edge in let otherID = edge.from == node.id ? edge.to : edge.from let otherTitle = graph.nodes.first(where: { $0.id == otherID })?.title ?? otherID Button { @@ -862,6 +864,9 @@ private struct NodeDetailView: View { } .buttonStyle(.plain) .help(L10n.t(OverviewStyle.explanation(for: edge.kind), appLanguage)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 2) + .background(TableZebra.color(for: index)) } } } diff --git a/RouterOSAssistant/Features/Wizard/Steps/Connect/ConnectView.swift b/RouterOSAssistant/Features/Wizard/Steps/Connect/ConnectView.swift index ab5675a..9cd4b9a 100644 --- a/RouterOSAssistant/Features/Wizard/Steps/Connect/ConnectView.swift +++ b/RouterOSAssistant/Features/Wizard/Steps/Connect/ConnectView.swift @@ -63,6 +63,7 @@ struct ConnectView: View { onDelete: { viewModel.removeSavedRouter(router.id) } ) .padding(.vertical, 6) + .background(TableZebra.color(for: index)) } } } @@ -351,7 +352,7 @@ struct ConnectView: View { } } Section("Interfaces") { - ForEach(connectionService.interfaces) { interface in + ForEach(Array(connectionService.interfaces.enumerated()), id: \.element.id) { index, interface in HStack { InterfaceActivityDot( running: interface.running, @@ -362,10 +363,12 @@ struct ConnectView: View { Text(interface.type).appFont(.caption).foregroundStyle(.secondary) } } + .listRowBackground(TableZebra.color(for: index)) } } } .formStyle(.grouped) + .scrollContentBackground(.hidden) } else { ContentUnavailableView( LocalizedStringKey(L10n.t("Nicht verbunden", appLanguage)), diff --git a/RouterOSAssistant/Features/Wizard/Steps/Setup/ReviewApplyView.swift b/RouterOSAssistant/Features/Wizard/Steps/Setup/ReviewApplyView.swift index 5906b82..b95c98c 100644 --- a/RouterOSAssistant/Features/Wizard/Steps/Setup/ReviewApplyView.swift +++ b/RouterOSAssistant/Features/Wizard/Steps/Setup/ReviewApplyView.swift @@ -10,7 +10,7 @@ struct ReviewApplyView: View { var body: some View { Form { Section(L10n.t("Geplante Änderungen", appLanguage)) { - ForEach(viewModel.plannedCommands) { command in + ForEach(Array(viewModel.plannedCommands.enumerated()), id: \.element.id) { index, command in VStack(alignment: .leading, spacing: 2) { Text(command.summary) if showCliDetails { @@ -19,6 +19,7 @@ struct ReviewApplyView: View { .foregroundStyle(.secondary) } } + .listRowBackground(TableZebra.color(for: index)) } Toggle(L10n.t("Details anzeigen (RouterOS-Befehle)", appLanguage), isOn: $showCliDetails) } @@ -31,8 +32,9 @@ struct ReviewApplyView: View { if !viewModel.applyLog.isEmpty { Section(L10n.t("Ablauf", appLanguage)) { - ForEach(viewModel.applyLog, id: \.self) { line in + ForEach(Array(viewModel.applyLog.enumerated()), id: \.offset) { index, line in Text(line).appFont(.caption) + .listRowBackground(TableZebra.color(for: index)) } } } @@ -70,6 +72,7 @@ struct ReviewApplyView: View { } } .formStyle(.grouped) + .scrollContentBackground(.hidden) .navigationTitle(LocalizedStringKey(L10n.t("Übersicht & Anwenden", appLanguage))) .alert( L10n.t("Anwenden fehlgeschlagen", appLanguage), diff --git a/found.md b/found.md index 09b80c9..ac523e6 100644 --- a/found.md +++ b/found.md @@ -170,6 +170,20 @@ Build+alle 99 Unit-Tests grün, live bestätigt. **Status:** offen ### 8. Abwechselnde Farbkombis bei Tabellenansichten -**Status:** offen +**Status:** fixed (live bestätigt: "passt") Bessere Lesbarkeit durch alternierende Zeilenfarben in Tabellenansichten (LAN-Scanner, Experte-Listen, etc.). + +Umsetzung: neuer `TableZebra`-Helper (`AppPreferences.swift`) nutzt AppKits eigenes `NSColor.alternatingContentBackgroundColors` (dieselbe Farbpaar, das `NSTableView` fürs Zebra-Streifenmuster verwendet) statt einer selbst gewählten Grauabstufung — passt sich automatisch an Hell/Dunkelmodus an. Angewendet über `.listRowBackground(TableZebra.color(for: index))` auf: LAN-Scanner-Geräte-Zeilen (`DevicesView.swift`, pro Port-Gruppe neu bei Index 0 startend) und Experte-Tab-Eintragsliste (`ExpertMenuDetailView.swift`). + +Bewusst nicht umgesetzt: "Weitere Parameter (frei)"-Grid im Experte-Bearbeiten-Sheet (Bug 2) — dort läuft die Zeilenzählung zweispaltig (zwei Einträge pro Grid-Zeile), Zebra-Streifen würden dort eher verwirren als helfen; bei Bedarf separat nachziehbar. + +Build grün, alle 99 Unit-Tests grün. + +Nachbesserung 1 (Live-Test: "keine Änderungen erkennbar") — Ursache mit Beweis gefunden statt weiter geraten: erst per `ImageRenderer`-Snapshot versucht (schlug fehl, `Form` rendert im Headless-Kontext leer — kein Ergebnis dazu), dann per Doku-Recherche bestätigt: `.formStyle(.grouped)` malt einen eigenen blickdichten Hintergrund über jede Section, der `.listRowBackground` komplett verdeckt — bekanntes, dokumentiertes SwiftUI-Verhalten, kein Fehler in der Farbberechnung. Fix: `.scrollContentBackground(.hidden)` auf beiden betroffenen `Form`s ergänzt (`DevicesView.swift`, `ExpertMenuDetailView.swift`), Standard-Kombination für genau diesen Fall. + +Build grün, alle 99 Unit-Tests grün. Achtung beim Live-Test: kann auch den "Karten"-Hintergrund der übrigen Form-Bereiche (nicht nur die Zeilen) sichtbar verändern — bitte Gesamteindruck prüfen, nicht nur ob Streifen da sind. + +Nachbesserung 2 (User-Feedback: auch das "Weitere Parameter"-Grid im Experte-Bearbeiten-Sheet soll Zeilen abwechselnd einfärben — Zeile 1,3,5... —, "bitte für das komplette Projekt umsetzen"): Grid-Zeilen (2 Parameter pro sichtbarer Zeile) bekommen jetzt `.background(TableZebra.color(for: rowIndex))` pro `GridRow`. Zusätzlich auf jede weitere echte Tabellen-/Listenansicht im Projekt ausgeweitet: "Bekannte Router"-Liste + Interface-Liste (Verbinden-Tab), Sicherungsliste (Sicherungen-Tab), Rohfelder-Sheet + Port-Scan-Ergebnisliste (LAN-Scanner), geplante Änderungen + Ablauf-Log (Einrichten-Wizard Review/Apply), Feld-Details + Verbindungsliste im Übersicht-Knoten-Detailpanel. Nicht angefasst: Experte-Sidebar (Navigationsliste, keine Datentabelle) und die Setup-Wizard-Konfigurationsformulare (VLAN/WLAN/LAN/WAN — je Zeile ein Mehrfeld-Unterformular, keine gleichförmigen Datenzeilen, Zebra-Streifen würden dort eher verwirren). + +Build grün, alle 99 Unit-Tests grün. Bitte erneut live testen.