Zebra-Streifen für Tabellenansichten (found.md #8)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user