From 3a6c0ba6c1e1e9bcd29c2fb628dc7f1037307ec5 Mon Sep 17 00:00:00 2001 From: Reed Es Date: Sat, 26 Feb 2022 21:49:44 -0700 Subject: [PATCH] Big changes. Supporting new grid-based list. (#1) --- README.md | 101 +++++++++--------- Sources/Grid/Internal/BaseGrid.swift | 53 +++++++++ .../Internal/BaseGridRow.swift} | 36 ++++--- Sources/Grid/TablerGrid.swift | 86 +++++++++++++++ Sources/Grid/TablerGridConfig.swift | 50 +++++++++ Sources/Internal/BaseTable.swift | 6 +- Sources/Internal/HeaderView.swift | 8 +- Sources/{ => List}/Internal/BaseList.swift | 9 +- Sources/{ => List}/Internal/BaseList1.swift | 9 +- Sources/{ => List}/Internal/BaseListM.swift | 9 +- Sources/List/Internal/BaseListRow.swift | 60 +++++++++++ Sources/List/TablerListConfig.swift | 14 ++- Sources/{ => Stack}/Internal/BaseStack.swift | 11 +- .../{ => Stack}/Internal/BaseStackRow.swift | 38 ++++--- .../{ => Stack}/Internal/BaseStackRow1.swift | 87 +++++++-------- Sources/Stack/TablerStackConfig.swift | 44 ++++---- Sources/TablerConfig.swift | 23 +++- 17 files changed, 465 insertions(+), 179 deletions(-) create mode 100644 Sources/Grid/Internal/BaseGrid.swift rename Sources/{Internal/BaseListRow.swift => Grid/Internal/BaseGridRow.swift} (56%) create mode 100644 Sources/Grid/TablerGrid.swift create mode 100644 Sources/Grid/TablerGridConfig.swift rename Sources/{ => List}/Internal/BaseList.swift (86%) rename Sources/{ => List}/Internal/BaseList1.swift (87%) rename Sources/{ => List}/Internal/BaseListM.swift (87%) create mode 100644 Sources/List/Internal/BaseListRow.swift rename Sources/{ => Stack}/Internal/BaseStack.swift (84%) rename Sources/{ => Stack}/Internal/BaseStackRow.swift (76%) rename Sources/{ => Stack}/Internal/BaseStackRow1.swift (60%) diff --git a/README.md b/README.md index 58cd1fc..4a6736f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A multi-platform SwiftUI component for displaying (and interacting with) tabular data. -Available as an open source Swift library to be incorporated in other apps. +Available as an open source library to be incorporated in SwiftUI apps. _SwiftTabular_ is part of the [OpenAlloc](https://github.com/openalloc) family of open source Swift software tools. @@ -21,12 +21,15 @@ macOS | iOS * No View type erasure (i.e., use of `AnyView`) which can impact scalability and performance * No external dependencies! -For `List`-based tables: +For List-based tables: * Optional moving of rows through drag and drop -* Support for no-select, single-select, and multi-select +* Support for single-select and multi-select -For `ScrollView`/`LazyVStack`-based tables: -* Support for no-select, single-select (possibily multi-select in future) +For ScrollView/LazyVStack-based tables: +* Support for single-select (possibily multi-select in future) + +For ScrollView/LazyVGrid-based tables: +* Likely the most scalable and efficient, but least flexible On macOS: * Hovering highlight, indicating which row the mouse is over @@ -66,25 +69,20 @@ struct ContentView: View { GridItem(.flexible(minimum: 35, maximum: 50), alignment: .leading), ] + @ViewBuilder private func header(_ ctx: TablerSortContext) -> some View { - LazyVGrid(columns: gridItems) { - Text("ID") - Text("Name") - Text("Weight") - Text("Color") - } - .padding(.horizontal) + Text("ID") + Text("Name") + Text("Weight") + Text("Color") } + @ViewBuilder private func row(_ element: Fruit) -> some View { - LazyVGrid(columns: gridItems) { - Text(element.id) - Text(element.name).foregroundColor(element.color) - Text(String(format: "%.0f g", element.weight)) - Image(systemName: "rectangle.fill").foregroundColor(element.color) - } - .padding(.horizontal) - .padding(.vertical, 5) + Text(element.id) + Text(element.name).foregroundColor(element.color) + Text(String(format: "%.0f g", element.weight)) + Image(systemName: "rectangle.fill").foregroundColor(element.color) } var body: some View { @@ -96,7 +94,7 @@ struct ContentView: View { } private var config: TablerListConfig { - TablerListConfig() + TablerListConfig(gridItems: gridItems) } } ``` @@ -105,23 +103,24 @@ struct ContentView: View { You can choose from any of ten (10) variants, which break down along the following lines: -* Both `List`-based and `ScrollView`/`LazyVStack`-based. -* Selection types offered: none, single-select, and multi-select (`List` only presently) +* List-based, ScrollView/LazyVStack-based, and ScrollView/LazyVGrid-based +* Selection types offered: none, single-select, and multi-select, depending on base * Unbound elements in row view, where you're presenting table rows read-only\* * Bound elements in row view, where you're presenting tables rows that can be updated directly (see Bound section below) -| Base | Selection of rows | Element wrapping | View name | -| --- | --- | --- | --- | -| `List` | No Select | (none) | `TablerList` | -| `List` | No Select | Binding\ | `TablerListB` | -| `List` | Single-select | (none) | `TablerList1` | -| `List` | Single-select | Binding\ | `TablerList1B` | -| `List` | Multi-select | (none) | `TablerListM` | -| `List` | Multi-select | Binding\ | `TablerListMB` | -| Stack | No Select | (none) | `TablerStack` | -| Stack | No Select | Binding\ | `TablerStackB` | -| Stack | Single-select | (none) | `TablerStack1` | -| Stack | Single-select | Binding\ | `TablerStack1B` | +Base | Selection of rows | Element wrapping | View name | Notes +--- | --- | --- | --- | --- +List | No Select | (none) | TablerList | +List | No Select | Binding\ | TablerListB | +List | Single-select | (none) | TablerList1 | +List | Single-select | Binding\ | TablerList1B | +List | Multi-select | (none) | TablerListM | +List | Multi-select | Binding\ | TablerListMB | +Stack | No Select | (none) | TablerStack | +Stack | No Select | Binding\ | TablerStackB | +Stack | Single-select | (none) | TablerStack1 | +Stack | Single-select | Binding\ | TablerStack1B | +Grid | No Select | (none) | TablerGrid | Experimental. Needs bound version, select, etc. \* 'unbound' variants can be used with Core Data (where values are bound by alternative means) @@ -132,16 +131,15 @@ Column sorting is available through `tablerSort` view function. From the demo app, an example of using the sort capability: ```swift +@ViewBuilder private func header(_ ctx: TablerSortContext) -> some View { - LazyVGrid(columns: gridItems) { - Text("ID \(Sort.indicator(ctx, \.id))") - .onTapGesture { tablerSort(ctx, &fruits, \.id) { $0.id < $1.id } } - Text("Name \(Sort.indicator(ctx, \.name))") - .onTapGesture { tablerSort(ctx, &fruits, \.name) { $0.name < $1.name } } - Text("Weight \(Sort.indicator(ctx, \.weight))") - .onTapGesture { tablerSort(ctx, &fruits, \.weight) { $0.weight < $1.weight } } - Text("Color") - } + Text("ID \(Sort.indicator(ctx, \.id))") + .onTapGesture { tablerSort(ctx, &fruits, \.id) { $0.id < $1.id } } + Text("Name \(Sort.indicator(ctx, \.name))") + .onTapGesture { tablerSort(ctx, &fruits, \.name) { $0.name < $1.name } } + Text("Weight \(Sort.indicator(ctx, \.weight))") + .onTapGesture { tablerSort(ctx, &fruits, \.weight) { $0.weight < $1.weight } } + Text("Color") } ``` @@ -156,15 +154,14 @@ macOS | iOS When used with 'bound' variants (e.g., `TablerListB`), the data can be modified directly, mutating your data source. From the demo: ```swift +@ViewBuilder private func brow(_ element: Binding) -> some View { - LazyVGrid(columns: gridItems) { - Text(element.wrappedValue.id) - TextField("Name", text: element.name) - .textFieldStyle(.roundedBorder) - Text(String(format: "%.0f g", element.wrappedValue.weight)) - ColorPicker("Color", selection: element.color) - .labelsHidden() - } + Text(element.wrappedValue.id) + TextField("Name", text: element.name) + .textFieldStyle(.roundedBorder) + Text(String(format: "%.0f g", element.wrappedValue.weight)) + ColorPicker("Color", selection: element.color) + .labelsHidden() } ``` diff --git a/Sources/Grid/Internal/BaseGrid.swift b/Sources/Grid/Internal/BaseGrid.swift new file mode 100644 index 0000000..9c2af7a --- /dev/null +++ b/Sources/Grid/Internal/BaseGrid.swift @@ -0,0 +1,53 @@ +// +// BaseGrid.swift +// +// Copyright 2022 FlowAllocator LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// Grid-based list +struct BaseGrid: View +where Element: Identifiable, + Header: View, + Rows: View +{ + typealias Config = TablerGridConfig + typealias HeaderContent = (Binding?>) -> Header + typealias RowContent = () -> Rows + + let config: Config + @ViewBuilder let headerContent: HeaderContent + @ViewBuilder let rowsContent: RowContent + + var body: some View { + BaseTable(config: config, + headerContent: headerContent) { buildHeader in + + VStack(spacing: config.rowSpacing) { + buildHeader() + + ScrollView { + LazyVGrid(columns: config.gridItems, + alignment: config.alignment, + spacing: config.rowSpacing) { + rowsContent() + } + } + } + .padding(config.paddingInsets) + } + } +} diff --git a/Sources/Internal/BaseListRow.swift b/Sources/Grid/Internal/BaseGridRow.swift similarity index 56% rename from Sources/Internal/BaseListRow.swift rename to Sources/Grid/Internal/BaseGridRow.swift index a18c254..51a4508 100644 --- a/Sources/Internal/BaseListRow.swift +++ b/Sources/Grid/Internal/BaseGridRow.swift @@ -1,5 +1,5 @@ // -// BaseListRow.swift +// BaseGridRow.swift // // Copyright 2022 FlowAllocator LLC // @@ -18,39 +18,43 @@ import SwiftUI -struct BaseListRow: View +// Row for stack-based list +struct BaseGridRow: View where Element: Identifiable, - Content: View + Row: View { - typealias Config = TablerListConfig + typealias Config = TablerGridConfig typealias Hovered = Element.ID? + typealias RowContent = () -> Row // MARK: Parameters var config: Config var element: Element @Binding var hovered: Hovered - var content: () -> Content + var rowContent: RowContent // MARK: Views var body: some View { let colorPair = config.onRowColor?(element) // NOTE okay if nil - content() - .moveDisabled(!config.canMove(element)) - + rowContent() + //.frame(maxWidth: .infinity, maxHeight: .infinity) .foregroundColor(colorPair?.0 ?? Color.primary) + // Colored rows get their background here. + // For non-colored rows, use accent color background to indicate selection. #if os(macOS) || targetEnvironment(macCatalyst) - // support hovering, but not for colored rows (yet) - // no background for colored rows (yet) - .onHover { if $0 { hovered = element.id } } - .background((colorPair == nil && hovered == element.id) - ? Color.accentColor.opacity(0.2) - : Color.clear) + // support hovering, but not for colored rows (yet) + .onHover { if $0 { hovered = element.id } } + + // If hovering, set the background here. + .background(colorPair?.1 ?? ( + hovered == element.id ? Color.accentColor.opacity(0.2) : Color.clear + )) + #elseif os(iOS) + .background(colorPair?.1 ?? Color.clear) #endif - - .listRowBackground(colorPair?.1 ?? Color.clear) } } diff --git a/Sources/Grid/TablerGrid.swift b/Sources/Grid/TablerGrid.swift new file mode 100644 index 0000000..74db176 --- /dev/null +++ b/Sources/Grid/TablerGrid.swift @@ -0,0 +1,86 @@ +// +// TablerGrid.swift +// +// Copyright 2022 FlowAllocator LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// Grid-based table +public struct TablerGrid: View +where Element: Identifiable, + Header: View, + Row: View, + Results: RandomAccessCollection, + Results.Element == Element +{ + public typealias Config = TablerGridConfig + public typealias Hovered = Element.ID? + public typealias HeaderContent = (Binding?>) -> Header + public typealias RowContent = (Element) -> Row + + // MARK: Parameters + + private let config: Config + private let headerContent: HeaderContent + private let rowContent: RowContent + private var results: Results + + public init(_ config: Config, + @ViewBuilder headerContent: @escaping HeaderContent, + @ViewBuilder rowContent: @escaping RowContent, + results: Results) + { + self.config = config + self.headerContent = headerContent + self.rowContent = rowContent + self.results = results + } + + // MARK: Locals + + @State private var hovered: Hovered = nil + + // MARK: Views + + public var body: some View { + BaseGrid(config: config, + headerContent: headerContent) { + ForEach(results.filter(config.filter ?? { _ in true })) { element in + BaseGridRow(config: config, + element: element, + hovered: $hovered) { + + // TODO how to provide a continuous hover block (selection, etc.)? + rowContent(element) + } + } + } + } +} + +public extension TablerGrid { + // omitting Header + init(_ config: Config, + @ViewBuilder rowContent: @escaping RowContent, + results: Results) + where Header == EmptyView + { + self.init(config, + headerContent: { _ in EmptyView() }, + rowContent: rowContent, + results: results) + } +} diff --git a/Sources/Grid/TablerGridConfig.swift b/Sources/Grid/TablerGridConfig.swift new file mode 100644 index 0000000..4e3d41f --- /dev/null +++ b/Sources/Grid/TablerGridConfig.swift @@ -0,0 +1,50 @@ +// +// TablerGridConfig.swift +// +// Copyright 2022 FlowAllocator LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +public enum TablerGridConfigDefaults { + + // TODO these values probably need to be tweaked to match the basic layout of `List` +#if os(macOS) + public static let rowSpacing: CGFloat = 8 + public static let paddingInsets = EdgeInsets(top: 14, leading: 16, bottom: 15, trailing: 16) +#elseif os(iOS) + public static let rowSpacing: CGFloat = 17 + public static let paddingInsets = EdgeInsets(top: 48, leading: 32, bottom: 20, trailing: 32) +#endif +} + +public class TablerGridConfig: TablerConfig +where Element: Identifiable +{ + public override init(gridItems: [GridItem], + alignment: HorizontalAlignment = TablerConfigDefaults.alignment, + filter: Filter? = nil, + onRowColor: OnRowColor? = nil, + rowSpacing: CGFloat = TablerGridConfigDefaults.rowSpacing, + paddingInsets: EdgeInsets = TablerGridConfigDefaults.paddingInsets) + { + super.init(gridItems: gridItems, + alignment: alignment, + filter: filter, + onRowColor: onRowColor, + rowSpacing: rowSpacing, + paddingInsets: paddingInsets) + } +} diff --git a/Sources/Internal/BaseTable.swift b/Sources/Internal/BaseTable.swift index 0a6f3a5..5b713c8 100644 --- a/Sources/Internal/BaseTable.swift +++ b/Sources/Internal/BaseTable.swift @@ -32,15 +32,15 @@ struct BaseTable: View // MARK: Parameters var config: Config - let headerContent: HeaderContent + var headerContent: HeaderContent var content: TableBuilder // MARK: Views var body: some View { content { - HeaderView(content: headerContent) + HeaderView(config: config, content: headerContent) } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + //.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } } diff --git a/Sources/Internal/HeaderView.swift b/Sources/Internal/HeaderView.swift index 75b7a68..c64584f 100644 --- a/Sources/Internal/HeaderView.swift +++ b/Sources/Internal/HeaderView.swift @@ -22,8 +22,11 @@ struct HeaderView: View where Element: Identifiable, Header: View { + typealias Config = TablerConfig + // MARK: Parameters + var config: Config @ViewBuilder var content: (Binding?>) -> Header // MARK: Locals @@ -34,6 +37,9 @@ struct HeaderView: View // MARK: Views var body: some View { - content($sort) + LazyVGrid(columns: config.gridItems, + alignment: config.alignment) { + content($sort) + } } } diff --git a/Sources/Internal/BaseList.swift b/Sources/List/Internal/BaseList.swift similarity index 86% rename from Sources/Internal/BaseList.swift rename to Sources/List/Internal/BaseList.swift index 4e067db..ee323d7 100644 --- a/Sources/Internal/BaseList.swift +++ b/Sources/List/Internal/BaseList.swift @@ -19,24 +19,25 @@ import SwiftUI // List with no selection -struct BaseList: View +struct BaseList: View where Element: Identifiable, Header: View, - Content: View + Rows: View { typealias Config = TablerListConfig typealias HeaderContent = (Binding?>) -> Header + typealias RowContent = () -> Rows let config: Config let headerContent: HeaderContent - @ViewBuilder var content: () -> Content + @ViewBuilder var rowsContent: RowContent var body: some View { BaseTable(config: config, headerContent: headerContent) { buildHeader in List { buildHeader() - content() + rowsContent() } } } diff --git a/Sources/Internal/BaseList1.swift b/Sources/List/Internal/BaseList1.swift similarity index 87% rename from Sources/Internal/BaseList1.swift rename to Sources/List/Internal/BaseList1.swift index 63654f2..6fc1612 100644 --- a/Sources/Internal/BaseList1.swift +++ b/Sources/List/Internal/BaseList1.swift @@ -19,26 +19,27 @@ import SwiftUI // List with single-selection -struct BaseList1: View +struct BaseList1: View where Element: Identifiable, Header: View, - Content: View + Rows: View { typealias Config = TablerListConfig typealias HeaderContent = (Binding?>) -> Header + typealias RowContent = () -> Rows typealias Selected = Element.ID? let config: Config let headerContent: HeaderContent @Binding var selected: Selected - @ViewBuilder var content: () -> Content + @ViewBuilder var rowsContent: RowContent var body: some View { BaseTable(config: config, headerContent: headerContent) { buildHeader in List(selection: $selected) { buildHeader() - content() + rowsContent() } } } diff --git a/Sources/Internal/BaseListM.swift b/Sources/List/Internal/BaseListM.swift similarity index 87% rename from Sources/Internal/BaseListM.swift rename to Sources/List/Internal/BaseListM.swift index 49098f3..1b2e67b 100644 --- a/Sources/Internal/BaseListM.swift +++ b/Sources/List/Internal/BaseListM.swift @@ -19,26 +19,27 @@ import SwiftUI // List with multi selection -struct BaseListM: View +struct BaseListM: View where Element: Identifiable, Header: View, - Content: View + Rows: View { typealias Config = TablerListConfig typealias HeaderContent = (Binding?>) -> Header // (Binding?>) -> Header + typealias RowContent = () -> Rows typealias Selected = Set let config: Config let headerContent: HeaderContent @Binding var selected: Selected - @ViewBuilder var content: () -> Content + @ViewBuilder var rowsContent: RowContent var body: some View { BaseTable(config: config, headerContent: headerContent) { buildHeader in List(selection: $selected) { buildHeader() - content() + rowsContent() } } } diff --git a/Sources/List/Internal/BaseListRow.swift b/Sources/List/Internal/BaseListRow.swift new file mode 100644 index 0000000..4d9df5c --- /dev/null +++ b/Sources/List/Internal/BaseListRow.swift @@ -0,0 +1,60 @@ +// +// BaseListRow.swift +// +// Copyright 2022 FlowAllocator LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct BaseListRow: View +where Element: Identifiable, + Row: View +{ + typealias Config = TablerListConfig + typealias Hovered = Element.ID? + typealias RowContent = () -> Row + + // MARK: Parameters + + var config: Config + var element: Element + @Binding var hovered: Hovered + var rowContent: RowContent + + // MARK: Views + + var body: some View { + let colorPair = config.onRowColor?(element) // NOTE okay if nil + + LazyVGrid(columns: config.gridItems, + alignment: config.alignment) { + rowContent() + } + .moveDisabled(!config.canMove(element)) + + .foregroundColor(colorPair?.0 ?? Color.primary) + +#if os(macOS) || targetEnvironment(macCatalyst) + // support hovering, but not for colored rows (yet) + // no background for colored rows (yet) + .onHover { if $0 { hovered = element.id } } + .background((colorPair == nil && hovered == element.id) + ? Color.accentColor.opacity(0.2) + : Color.clear) +#endif + + .listRowBackground(colorPair?.1 ?? Color.clear) + } +} diff --git a/Sources/List/TablerListConfig.swift b/Sources/List/TablerListConfig.swift index e574378..fed4331 100644 --- a/Sources/List/TablerListConfig.swift +++ b/Sources/List/TablerListConfig.swift @@ -19,22 +19,26 @@ import SwiftUI public class TablerListConfig: TablerConfig - where Element: Identifiable +where Element: Identifiable { public typealias CanMove = (Element) -> Bool public typealias OnMove = (IndexSet, Int) -> Void - + public let canMove: CanMove public let onMove: OnMove? - - public init(filter: Filter? = nil, + + public init(gridItems: [GridItem], + alignment: HorizontalAlignment = TablerConfigDefaults.alignment, + filter: Filter? = nil, onRowColor: OnRowColor? = nil, canMove: @escaping CanMove = { _ in true }, onMove: OnMove? = nil) { self.canMove = canMove self.onMove = onMove - super.init(filter: filter, + super.init(gridItems: gridItems, + alignment: alignment, + filter: filter, onRowColor: onRowColor) } } diff --git a/Sources/Internal/BaseStack.swift b/Sources/Stack/Internal/BaseStack.swift similarity index 84% rename from Sources/Internal/BaseStack.swift rename to Sources/Stack/Internal/BaseStack.swift index 65c6397..a4de519 100644 --- a/Sources/Internal/BaseStack.swift +++ b/Sources/Stack/Internal/BaseStack.swift @@ -19,28 +19,29 @@ import SwiftUI // Stack-based list -struct BaseStack: View +struct BaseStack: View where Element: Identifiable, Header: View, - Content: View + Rows: View { typealias Config = TablerStackConfig typealias HeaderContent = (Binding?>) -> Header + typealias RowContent = () -> Rows let config: Config let headerContent: HeaderContent - @ViewBuilder var content: () -> Content + @ViewBuilder var rowsContent: RowContent var body: some View { BaseTable(config: config, headerContent: headerContent) { buildHeader in - VStack(spacing: config.headerSpacing) { + VStack(spacing: config.rowSpacing) { buildHeader() ScrollView { LazyVStack(alignment: .leading, spacing: config.rowSpacing) { - content() + rowsContent() } } } diff --git a/Sources/Internal/BaseStackRow.swift b/Sources/Stack/Internal/BaseStackRow.swift similarity index 76% rename from Sources/Internal/BaseStackRow.swift rename to Sources/Stack/Internal/BaseStackRow.swift index 1871daf..276acfa 100644 --- a/Sources/Internal/BaseStackRow.swift +++ b/Sources/Stack/Internal/BaseStackRow.swift @@ -19,42 +19,46 @@ import SwiftUI // Row for stack-based list -struct BaseStackRow: View - where Element: Identifiable, - Content: View +struct BaseStackRow: View +where Element: Identifiable, + Row: View { typealias Config = TablerStackConfig typealias Hovered = Element.ID? - + typealias RowContent = () -> Row + // MARK: Parameters - + var config: Config var element: Element @Binding var hovered: Hovered - var content: () -> Content - + var content: RowContent + // MARK: Locals - + // MARK: Views - + var body: some View { let colorPair = config.onRowColor?(element) // NOTE okay if nil - - content() - .foregroundColor(colorPair?.0 ?? Color.primary) - + + LazyVGrid(columns: config.gridItems, + alignment: config.alignment) { + content() + } + .foregroundColor(colorPair?.0 ?? Color.primary) + // Colored rows get their background here. // For non-colored rows, use accent color background to indicate selection. - #if os(macOS) || targetEnvironment(macCatalyst) +#if os(macOS) || targetEnvironment(macCatalyst) // support hovering, but not for colored rows (yet) .onHover { if $0 { hovered = element.id } } - + // If hovering, set the background here. .background(colorPair?.1 ?? ( hovered == element.id ? Color.accentColor.opacity(0.2) : Color.clear )) - #elseif os(iOS) +#elseif os(iOS) .background(colorPair?.1 ?? Color.clear) - #endif +#endif } } diff --git a/Sources/Internal/BaseStackRow1.swift b/Sources/Stack/Internal/BaseStackRow1.swift similarity index 60% rename from Sources/Internal/BaseStackRow1.swift rename to Sources/Stack/Internal/BaseStackRow1.swift index 787315d..51768ae 100644 --- a/Sources/Internal/BaseStackRow1.swift +++ b/Sources/Stack/Internal/BaseStackRow1.swift @@ -19,75 +19,78 @@ import SwiftUI // Row for stack-based list, with support for single-select -struct BaseStackRow1: View - where Element: Identifiable, - Content: View +struct BaseStackRow1: View +where Element: Identifiable, + Row: View { typealias Config = TablerStackConfig typealias Hovered = Element.ID? typealias Selected = Element.ID? - + typealias RowContent = () -> Row + // MARK: Parameters - + var config: Config var element: Element @Binding var hovered: Hovered @Binding var selected: Selected - var content: () -> Content - + var rowContent: RowContent + // MARK: Locals - + // MARK: Views - + var body: some View { let colorPair = config.onRowColor?(element) // NOTE okay if nil - - content() - - .foregroundColor(colorPair?.0 ?? Color.primary) - -// .overlay( -// SwSelectBorder(colorPair != nil && element == selected) -// ) - - #if os(macOS) || targetEnvironment(macCatalyst) + + LazyVGrid(columns: config.gridItems, + alignment: config.alignment) { + rowContent() + } + .foregroundColor(colorPair?.0 ?? Color.primary) + + // .overlay( + // SwSelectBorder(colorPair != nil && element == selected) + // ) + +#if os(macOS) || targetEnvironment(macCatalyst) // NOTE keeping selection part of mac, as on iOS you press to get the context menu - .contentShape(Rectangle()) - .onTapGesture { - selectAction(element) -} - // .onLongPressGesture { - // selectAction(element) - // guard canEdit else { return } - // toEdit = element - // } - #endif - + .contentShape(Rectangle()) + .onTapGesture { + selectAction(element) + } + // .onLongPressGesture { + // selectAction(element) + // guard canEdit else { return } + // toEdit = element + // } +#endif + // Colored rows get their background here. // For non-colored rows, use accent color background to indicate selection. - #if os(macOS) || targetEnvironment(macCatalyst) +#if os(macOS) || targetEnvironment(macCatalyst) // support hovering, but not for colored rows (yet) .onHover { if $0 { hovered = element.id } } - + // If hovering, set the background here. .background(colorPair?.1 ?? ( element.id == selected ? Color.accentColor : ( hovered == element.id ? Color.accentColor.opacity(0.2) : Color.clear ) )) - #elseif os(iOS) +#elseif os(iOS) .background(colorPair?.1 ?? ( element.id == selected ? Color.accentColor : Color.clear )) - #endif +#endif } - + // MARK: Action Handlers - - #if os(macOS) || targetEnvironment(macCatalyst) - private func selectAction(_ element: Element) { - selected = element.id - // onSelect?(element) - } - #endif + +#if os(macOS) || targetEnvironment(macCatalyst) + private func selectAction(_ element: Element) { + selected = element.id + // onSelect?(element) + } +#endif } diff --git a/Sources/Stack/TablerStackConfig.swift b/Sources/Stack/TablerStackConfig.swift index 78bdeca..89f0043 100644 --- a/Sources/Stack/TablerStackConfig.swift +++ b/Sources/Stack/TablerStackConfig.swift @@ -20,34 +20,30 @@ import SwiftUI public enum TablerStackConfigDefaults { // approximately match the layout of Stack - #if os(macOS) - public static let headerSpacing: CGFloat = 8 - public static let rowSpacing: CGFloat = 8 - public static let paddingInsets = EdgeInsets(top: 14, leading: 16, bottom: 15, trailing: 16) - #elseif os(iOS) - public static let headerSpacing: CGFloat = 17 - public static let rowSpacing: CGFloat = 17 - public static let paddingInsets = EdgeInsets(top: 48, leading: 32, bottom: 20, trailing: 32) - #endif +#if os(macOS) + public static let rowSpacing: CGFloat = 8 + public static let paddingInsets = EdgeInsets(top: 14, leading: 16, bottom: 15, trailing: 16) +#elseif os(iOS) + public static let rowSpacing: CGFloat = 17 + public static let paddingInsets = EdgeInsets(top: 48, leading: 32, bottom: 20, trailing: 32) +#endif } public class TablerStackConfig: TablerConfig - where Element: Identifiable +where Element: Identifiable { - public let headerSpacing: CGFloat - public let rowSpacing: CGFloat - public let paddingInsets: EdgeInsets - - public init(filter: Filter? = nil, - onRowColor: OnRowColor? = nil, - headerSpacing: CGFloat = TablerStackConfigDefaults.headerSpacing, - rowSpacing: CGFloat = TablerStackConfigDefaults.rowSpacing, - paddingInsets: EdgeInsets = TablerStackConfigDefaults.paddingInsets) + public override init(gridItems: [GridItem], + alignment: HorizontalAlignment = TablerConfigDefaults.alignment, + filter: Filter? = nil, + onRowColor: OnRowColor? = nil, + rowSpacing: CGFloat = TablerStackConfigDefaults.rowSpacing, + paddingInsets: EdgeInsets = TablerStackConfigDefaults.paddingInsets) { - self.headerSpacing = headerSpacing - self.rowSpacing = rowSpacing - self.paddingInsets = paddingInsets - super.init(filter: filter, - onRowColor: onRowColor) + super.init(gridItems: gridItems, + alignment: alignment, + filter: filter, + onRowColor: onRowColor, + rowSpacing: rowSpacing, + paddingInsets: paddingInsets) } } diff --git a/Sources/TablerConfig.swift b/Sources/TablerConfig.swift index 6bdd270..4bca1eb 100644 --- a/Sources/TablerConfig.swift +++ b/Sources/TablerConfig.swift @@ -18,6 +18,13 @@ import SwiftUI +public enum TablerConfigDefaults { + public static let headerSpacing: CGFloat = 0 + public static let rowSpacing: CGFloat = 0 + public static let paddingInsets = EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) + public static let alignment: HorizontalAlignment = .leading +} + open class TablerConfig where Element: Identifiable { @@ -29,13 +36,25 @@ open class TablerConfig // MARK: Parameters + public let gridItems: [GridItem] + public let alignment: HorizontalAlignment public let filter: Filter? public let onRowColor: OnRowColor? + public let rowSpacing: CGFloat + public let paddingInsets: EdgeInsets - public init(filter: Filter? = nil, - onRowColor: OnRowColor? = nil) + public init(gridItems: [GridItem], + alignment: HorizontalAlignment = TablerConfigDefaults.alignment, + filter: Filter? = nil, + onRowColor: OnRowColor? = nil, + rowSpacing: CGFloat = TablerConfigDefaults.rowSpacing, + paddingInsets: EdgeInsets = TablerConfigDefaults.paddingInsets) { + self.gridItems = gridItems + self.alignment = alignment self.filter = filter self.onRowColor = onRowColor + self.rowSpacing = rowSpacing + self.paddingInsets = paddingInsets } }