Skip to content

Commit

Permalink
TablerGridC and README improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
Reed Es committed Mar 5, 2022
1 parent 2c6b82a commit a883c07
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 35 deletions.
116 changes: 84 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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\<Element>
List | No Select | | ✓ | | TablerListC | ObservedObject
List | Single-select | ✓ | ✓ | ✓ | TablerList1 | (none)
List | Single-select | ✓ | | ✓ | TablerList1B | Binding\<Element>
List | Single-Select | | ✓ | | TablerList1C | ObservedObject
List | Multi-select | ✓ | ✓ | ✓ | TablerListM | (none)
List | Multi-select | ✓ | | ✓ | TablerListMB | Binding\<Element>
List | Multi-select | | ✓ | | TablerListMC | ObservedObject
Stack | No Select | ✓ | ✓ | ✓ | TablerStack | (none)
Stack | No Select | ✓ | | ✓ | TablerStackB | Binding\<Element>
Stack | No Select | | ✓ | | TablerStackC | ObservedObject
Stack | Single-select | ✓ | ✓ | ✓ | TablerStack1 | (none)
Stack | Single-select | ✓ | | ✓ | TablerStack1B | Binding\<Element>
Stack | Single-select | | ✓ | | TablerStack1C | ObservedObject
Grid | No Select | ✓ | ✓ | ✓ | TablerGrid | (none)
Grid | No Select | ✓ | ✓ | | TablerGridB | Binding\<Element
Grid | No Select | ✓ | ✓ | | TablerGridC | ObservedObject
* Base - three foundations: `List`, `ScrollView/LazyVStack`, and `ScrollView/LazyVGrid`
* Select - single-select, and multi-select, or selection not supported
* RAC - can be used with `RandomAccessCollection` (e.g., array of struct)
* CD - can be used with Core Data
* Filter - is `config.filter` supported?
* Bound - the mechanism through which values are bound, if at all
* View - the view to use

Base | Select | RAC | CD | Filter | Bound | View
--- | --- | --- | --- | --- | --- | ---
List | | ✓ | ✓ | ✓ | | TablerList
List | | ✓ | | ✓\* | Binding\<Element> | TablerListB
List | | | ✓ | | NSManagedObject | TablerListC
List | Single | ✓ | ✓ | ✓ | | TablerList1
List | Single | ✓ | | ✓\* | Binding\<Element> | TablerList1B
List | Single | | ✓ | | NSManagedObject | TablerList1C
List | Multi | ✓ | ✓ | ✓ | | TablerListM
List | Multi | ✓ | | ✓\* | Binding\<Element> | TablerListMB
List | Multi | | ✓ | | NSManagedObject | TablerListMC
Stack | | ✓ | ✓ | ✓ | | TablerStack
Stack | | ✓ | | ✓\* | Binding\<Element> | TablerStackB
Stack | | | ✓ | | NSManagedObject | TablerStackC
Stack | Single | ✓ | ✓ | ✓ | | TablerStack1
Stack | Single | ✓ | | ✓\* | Binding\<Element> | TablerStack1B
Stack | Single | | ✓ | | NSManagedObject | TablerStack1C
Grid | | ✓ | ✓ | ✓ | | TablerGrid
Grid | | ✓ | ✓ | | Binding\<Element> | 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<Fruit>
Expand All @@ -156,9 +172,25 @@ private func header(ctx: Binding<Context>) -> 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<Fruit>
private typealias Sort = TablerSort<Fruit>

private func header(ctx: Binding<Context>) -> 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

Expand All @@ -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<Fruit>) -> some View {
private func brow(element: BoundValue) -> some View {
LazyVGrid(columns: gridItems) {
Text(element.wrappedValue.id)
TextField("Name", text: element.name)
Expand All @@ -181,6 +213,26 @@ private func brow(element: Binding<Fruit>) -> some View {
}
```

### Random Access Collection

For Random Access Collection sources, `BoundValue` is:

```swift
typealias BoundValue = Binding<Fruit>
```

### Core Data

For Core Data sources, `BoundValue` is:

```swift
typealias BoundValue = ObservedObject<Fruit>.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
Expand Down
2 changes: 1 addition & 1 deletion Sources/Grid/Internal/BaseGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ struct BaseGrid<Element, Header, Rows>: View
BaseTable(context: $context,
header: headerContent) { buildHeader in

VStack(spacing: config.rowSpacing) {
VStack(spacing: config.rowSpacing) { //TODO headerSpacing
buildHeader()

ScrollView {
Expand Down
122 changes: 122 additions & 0 deletions Sources/Grid/TablerGridC.swift
Original file line number Diff line number Diff line change
@@ -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<Element, Header, Row, RowBack>: View
where Element: Identifiable & NSFetchRequestResult & ObservableObject,
Header: View,
Row: View,
RowBack: View
{
public typealias Config = TablerGridConfig<Element>
public typealias Context = TablerContext<Element>
public typealias Hovered = Element.ID?
public typealias HeaderContent = (Binding<Context>) -> Header
public typealias ProjectedValue = ObservedObject<Element>.Wrapper
public typealias RowContent = (ProjectedValue) -> Row
public typealias RowBackground = (Element) -> RowBack
public typealias Fetched = FetchedResults<Element>

// 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)
}

}
2 changes: 1 addition & 1 deletion Sources/Stack/TablerStack1C.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Element, Header, Row, RowBack, Select>: View
where Element: Identifiable & NSFetchRequestResult & ObservableObject,
Header: View,
Expand Down
2 changes: 1 addition & 1 deletion Sources/Stack/TablerStackC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Element, Header, Row, RowBack>: View
where Element: Identifiable & NSFetchRequestResult & ObservableObject,
Header: View,
Expand Down

0 comments on commit a883c07

Please sign in to comment.