diff --git a/README.md b/README.md index 49b158b..9fc4766 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,15 @@ For ScrollView/LazyVGrid-based tables: On macOS: * Hovering highlight, indicating which row the mouse is over +Notes: + \* Other platforms like macCatalyst, iPad on Mac, watchOS, tvOS, etc. are poorly supported, if at all. Please contribute to improve support! \*\* AnyView only used to specify sort configuration images in configuration, which shouldn't impact scalability. ## Tabler Example -The basic example below shows the display of tabular data using `TablerList`, which is for the display of unbound data without any selection capability. +The example below shows the display of tabular data from an array using `TablerList`, which is for the display of unbound data without any selection capability. ```swift import SwiftUI @@ -102,42 +104,56 @@ struct ContentView: View { } ``` -## Tables +While `LazyVGrid` is used to wrap the header and row items, you could alternatively wrap them with `HStack` or similar mechanism. -You can choose from any of sixteen (16) variants, which break down along the following lines: +## Tabler Views -* Three foundations: List-based, ScrollView/LazyVStack-based, and ScrollView/LazyVGrid-based -* Selection types offered: none, single-select, and multi-select; availability depending on base -* RAC - usable with `RandomAccessCollection` (e.g., array of struct), with or without binding -* CD - usable with Core Data, with or without binding -* Filter - is `config.filter` supported? +You can choose from any of eighteen (18) variants, which break down along the following lines: -Base | Row Selection | RAC | CD | Filter | View name | Element wrapping ---- | --- | --- | --- | --- | --- | --- -List | No Select | ✓ | ✓ | ✓ | TablerList | (none) -List | No Select | ✓ | | ✓ | TablerListB | Binding\ -List | No Select | | ✓ | | TablerListC | ObservedObject -List | Single-select | ✓ | ✓ | ✓ | TablerList1 | (none) -List | Single-select | ✓ | | ✓ | TablerList1B | Binding\ -List | Single-Select | | ✓ | | TablerList1C | ObservedObject -List | Multi-select | ✓ | ✓ | ✓ | TablerListM | (none) -List | Multi-select | ✓ | | ✓ | TablerListMB | Binding\ -List | Multi-select | | ✓ | | TablerListMC | ObservedObject -Stack | No Select | ✓ | ✓ | ✓ | TablerStack | (none) -Stack | No Select | ✓ | | ✓ | TablerStackB | Binding\ -Stack | No Select | | ✓ | | TablerStackC | ObservedObject -Stack | Single-select | ✓ | ✓ | ✓ | TablerStack1 | (none) -Stack | Single-select | ✓ | | ✓ | TablerStack1B | Binding\ -Stack | Single-select | | ✓ | | TablerStack1C | ObservedObject -Grid | No Select | ✓ | ✓ | ✓ | TablerGrid | (none) -Grid | No Select | ✓ | ✓ | | TablerGridB | Binding\ | TablerListB +List | | | ✓ | | NSManagedObject | TablerListC +List | Single | ✓ | ✓ | ✓ | | TablerList1 +List | Single | ✓ | | ✓\* | Binding\ | TablerList1B +List | Single | | ✓ | | NSManagedObject | TablerList1C +List | Multi | ✓ | ✓ | ✓ | | TablerListM +List | Multi | ✓ | | ✓\* | Binding\ | TablerListMB +List | Multi | | ✓ | | NSManagedObject | TablerListMC +Stack | | ✓ | ✓ | ✓ | | TablerStack +Stack | | ✓ | | ✓\* | Binding\ | TablerStackB +Stack | | | ✓ | | NSManagedObject | TablerStackC +Stack | Single | ✓ | ✓ | ✓ | | TablerStack1 +Stack | Single | ✓ | | ✓\* | Binding\ | TablerStack1B +Stack | Single | | ✓ | | NSManagedObject | TablerStack1C +Grid | | ✓ | ✓ | ✓ | | TablerGrid +Grid | | ✓ | ✓ | | Binding\ | TablerGridB +Grid | | ✓ | ✓ | | NSManagedObject | TablerGridC + +\* filtering with Binding-based data likely not scalable as implemented. If you can find a better way to implement, submit a pull request! ## Column Sorting Column sorting is available through `tablerSort` view function. -From the demo app, an example of using the sort capability, where an indicator displays in the header if the column is actively sorted: +The example below show how the header items can support sort. + +The `columnTitle` is a convenience function that displays header name along with an indicator showing the current sort, if any. + +Caret images are used by default, but are configurable in `TablerConfig`. + +### Random Access Collection + +From the _TablerDemo_ app: ```swift private typealias Context = TablerContext @@ -156,9 +172,25 @@ private func header(ctx: Binding) -> some View { } ``` -When the user clicks on a header column for the first time, it is sorted in ascending order, with an up-chevron "^" indicator. If clicked a successive time, a descending sort is executed, with a down-chevron "v" indicator. See `TablerConfig` for configuration. +### Core Data + +The sort method used with Core Data differs. From the _TablerCoreDemo_ app: -For sorting with Core Data, see the _TablerCoreDemo_ app. +```swift +private typealias Context = TablerContext +private typealias Sort = TablerSort + +private func header(ctx: Binding) -> some View { + LazyVGrid(columns: gridItems, alignment: .leading) { + Sort.columnTitle("ID", ctx, \.id) + .onTapGesture { fruits.sortDescriptors = [tablerSort(ctx, \.id)] } + Sort.columnTitle("Name", ctx, \.name) + .onTapGesture { fruits.sortDescriptors = [tablerSort(ctx, \.name)] } + Sort.columnTitle("Weight", ctx, \.weight) + .onTapGesture { fruits.sortDescriptors = [tablerSort(ctx, \.weight)] } + } +} +``` ## Bound data @@ -169,7 +201,7 @@ macOS | iOS When used with 'bound' variants (e.g., `TablerListB`), the data can be modified directly, mutating your data source. From the demo: ```swift -private func brow(element: Binding) -> some View { +private func brow(element: BoundValue) -> some View { LazyVGrid(columns: gridItems) { Text(element.wrappedValue.id) TextField("Name", text: element.name) @@ -181,6 +213,26 @@ private func brow(element: Binding) -> some View { } ``` +### Random Access Collection + +For Random Access Collection sources, `BoundValue` is: + +```swift +typealias BoundValue = Binding +``` + +### Core Data + +For Core Data sources, `BoundValue` is: + +```swift +typealias BoundValue = ObservedObject.Wrapper +``` + +Also known as `ProjectedValue`. + +Note that for Core Data, the user's changes will need to be committed to the Managed Object Context. See the _TablerCoreData_ code for an example of how this might be done. + ## Row Background macOS | iOS diff --git a/Sources/Grid/Internal/BaseGrid.swift b/Sources/Grid/Internal/BaseGrid.swift index 7ca7705..3d073c7 100644 --- a/Sources/Grid/Internal/BaseGrid.swift +++ b/Sources/Grid/Internal/BaseGrid.swift @@ -46,7 +46,7 @@ struct BaseGrid: View BaseTable(context: $context, header: headerContent) { buildHeader in - VStack(spacing: config.rowSpacing) { + VStack(spacing: config.rowSpacing) { //TODO headerSpacing buildHeader() ScrollView { diff --git a/Sources/Grid/TablerGridC.swift b/Sources/Grid/TablerGridC.swift new file mode 100644 index 0000000..f383d63 --- /dev/null +++ b/Sources/Grid/TablerGridC.swift @@ -0,0 +1,122 @@ +// +// TablerGridC.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, with support for bound values through Core Data +public struct TablerGridC: View + where Element: Identifiable & NSFetchRequestResult & ObservableObject, + Header: View, + Row: View, + RowBack: View +{ + public typealias Config = TablerGridConfig + public typealias Context = TablerContext + public typealias Hovered = Element.ID? + public typealias HeaderContent = (Binding) -> Header + public typealias ProjectedValue = ObservedObject.Wrapper + public typealias RowContent = (ProjectedValue) -> Row + public typealias RowBackground = (Element) -> RowBack + public typealias Fetched = FetchedResults + + // MARK: Parameters + + private let config: Config + private let headerContent: HeaderContent + private let rowContent: RowContent + private let rowBackground: RowBackground + private var results: Fetched + + public init(_ config: Config = .init(), + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Fetched) + { + self.config = config + headerContent = header + rowContent = row + self.rowBackground = rowBackground + self.results = results + _context = State(initialValue: TablerContext(config)) + } + + // MARK: Locals + + @State private var hovered: Hovered = nil + @State private var context: Context + + // MARK: Views + + public var body: some View { + BaseGrid(context: $context, + header: headerContent) { + ForEach(results) { rawElem in + ObservableHolder(element: rawElem) { obsElem in + rowContent(obsElem) + .modifier(GridItemMod(config, rawElem, $hovered)) + .background(rowBackground(rawElem)) + } + } + } + } +} + +public extension TablerGridC { + // omitting Header + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Fetched) + where Header == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + results: results) + } + + // omitting Background + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + results: Fetched) + where RowBack == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + results: results) + } + + // omitting Header AND Background + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + results: Fetched) + where Header == EmptyView, RowBack == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + results: results) + } + +} diff --git a/Sources/Stack/TablerStack1C.swift b/Sources/Stack/TablerStack1C.swift index a0ecf1e..dab9e6c 100644 --- a/Sources/Stack/TablerStack1C.swift +++ b/Sources/Stack/TablerStack1C.swift @@ -19,7 +19,7 @@ import CoreData import SwiftUI -/// Stack-based table, with support for bound values +/// Stack-based table, with support for bound values through Core Data public struct TablerStack1C: View where Element: Identifiable & NSFetchRequestResult & ObservableObject, Header: View, diff --git a/Sources/Stack/TablerStackC.swift b/Sources/Stack/TablerStackC.swift index 7853bd0..9a23391 100644 --- a/Sources/Stack/TablerStackC.swift +++ b/Sources/Stack/TablerStackC.swift @@ -19,7 +19,7 @@ import CoreData import SwiftUI -/// Stack-based table, with support for bound values +/// Stack-based table, with support for bound values through Core Data public struct TablerStackC: View where Element: Identifiable & NSFetchRequestResult & ObservableObject, Header: View,