From 899222f5f53e3c601fd4ae50644a023adbc1473d Mon Sep 17 00:00:00 2001 From: Reed Es Date: Sun, 6 Mar 2022 20:07:38 -0700 Subject: [PATCH] Six new multi-select variants; simple select/unselect for Stack/Grid --- README.md | 16 +- Sources/Grid/Internal/GridItemMod1.swift | 14 +- Sources/Grid/Internal/GridItemModM.swift | 54 ++++++ Sources/Grid/TablerGrid1.swift | 2 +- Sources/Grid/TablerGrid1B.swift | 2 +- Sources/Grid/TablerGrid1C.swift | 1 - Sources/Grid/TablerGridB.swift | 2 +- Sources/Grid/TablerGridC.swift | 1 - Sources/Grid/TablerGridM.swift | 212 ++++++++++++++++++++ Sources/Grid/TablerGridMB.swift | 213 ++++++++++++++++++++ Sources/Grid/TablerGridMC.swift | 215 ++++++++++++++++++++ Sources/List/TablerList1B.swift | 2 +- Sources/List/TablerList1C.swift | 3 +- Sources/List/TablerListB.swift | 2 +- Sources/List/TablerListC.swift | 1 - Sources/List/TablerListMB.swift | 2 +- Sources/List/TablerListMC.swift | 3 +- Sources/Stack/Internal/StackRowMod1.swift | 12 +- Sources/Stack/Internal/StackRowModM.swift | 53 +++++ Sources/Stack/TablerStack1B.swift | 2 +- Sources/Stack/TablerStack1C.swift | 1 - Sources/Stack/TablerStackB.swift | 2 +- Sources/Stack/TablerStackC.swift | 1 - Sources/Stack/TablerStackM.swift | 212 ++++++++++++++++++++ Sources/Stack/TablerStackMB.swift | 226 ++++++++++++++++++++++ Sources/Stack/TablerStackMC.swift | 215 ++++++++++++++++++++ 26 files changed, 1441 insertions(+), 28 deletions(-) create mode 100644 Sources/Grid/Internal/GridItemModM.swift create mode 100644 Sources/Grid/TablerGridM.swift create mode 100644 Sources/Grid/TablerGridMB.swift create mode 100644 Sources/Grid/TablerGridMC.swift create mode 100644 Sources/Stack/Internal/StackRowModM.swift create mode 100644 Sources/Stack/TablerStackM.swift create mode 100644 Sources/Stack/TablerStackMB.swift create mode 100644 Sources/Stack/TablerStackMC.swift diff --git a/README.md b/README.md index 2f2a595..adb62cd 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ macOS | iOS * Presently targeting macOS v11+ and iOS v14+\* * Supporting both value and reference semantics (including Core Data, which uses the latter) * Option to support a bound data source, where inline controls can directly mutate your data model +* Support for single-select, multi-select, or no selection * Option to sort by column, with indicators and concise syntax -* Option to specify a row background -* Option to specify a row overlay +* Option to specify a row background and/or overlay * On macOS, option for a hovering highlight, to indicate which row the mouse is over * MINIMAL use of View erasure (i.e., use of `AnyView`), which can impact scalability and performance\*\* * No external dependencies! @@ -28,11 +28,9 @@ Three table types are supported, as determined by the mechanism by which their h ### List * Based on SwiftUI's `List` * Option to support moving of rows through drag and drop -* Support for single-select, multi-select, or no selection at all ### Stack * Based on `ScrollView`/`LazyVStack` -* Support for single-select and no selection at all ### Grid * Based on `ScrollView`/`LazyVGrid` @@ -107,14 +105,14 @@ While `LazyVGrid` is used here to wrap the header and row items, you could alter ## Tabler Views -_Tabler_ offers twenty-one (21) variants of table views from which you can choose. They break down along the following lines: +_Tabler_ offers twenty-seven (27) variants of table views from which you can choose. They break down along the following lines: * Table View - the View name * Type - each of the three table types differ in how they render: - **List** - based on `List` - **Stack** - based on `ScrollView`/`LazyVStack` - **Grid** - based on `ScrollView`/`LazyVGrid` -* Select - single-select, multi-select, or selection not supported +* Select - single-select, multi-select, or no selection * Value - if checked, can be used with value types (e.g., struct values) * Reference - if checked, can be used with reference types (e.g., class objects, Core Data, etc.) * Filter - if checked, `config.filter` is supported (see caveat below) @@ -136,12 +134,18 @@ Table View | Type | Select | Value | Reference | Filter `TablerStack1` | **Stack** | Single | ✓ | ✓ | ✓ `TablerStack1B` | **Stack** | Single | ✓ | | ✓\* `TablerStack1C` | **Stack** | Single | | ✓ | +`TablerStackM` | **Stack** | Multi | ✓ | ✓ | ✓ +`TablerStackMB` | **Stack** | Multi | ✓ | | ✓\* +`TablerStackMC` | **Stack** | Multi | | ✓ | `TablerGrid` | **Grid** | | ✓ | ✓ | ✓ `TablerGridB` | **Grid** | | ✓ | | `TablerGridC` | **Grid** | | | ✓ | `TablerGrid1` | **Grid** | Single | ✓ | ✓ | ✓ `TablerGrid1B` | **Grid** | Single | ✓ | | `TablerGrid1C` | **Grid** | Single | | ✓ | +`TablerGridM` | **Grid** | Multi | ✓ | ✓ | ✓ +`TablerGridMB` | **Grid** | Multi | ✓ | | +`TablerGridMC` | **Grid** | Multi | | ✓ | \* filtering with bound values likely not scalable as implemented. If you can find a better way to implement, please submit a pull request! diff --git a/Sources/Grid/Internal/GridItemMod1.swift b/Sources/Grid/Internal/GridItemMod1.swift index d7c338a..01b021c 100644 --- a/Sources/Grid/Internal/GridItemMod1.swift +++ b/Sources/Grid/Internal/GridItemMod1.swift @@ -1,5 +1,5 @@ // -// GridItemMod.swift +// GridItemMod1.swift // // Copyright 2022 FlowAllocator LLC // @@ -18,6 +18,7 @@ import SwiftUI +/// Support for single-select Grid-based rows struct GridItemMod1: ViewModifier where Element: Identifiable { @@ -34,10 +35,17 @@ struct GridItemMod1: ViewModifier content .padding(config.itemPadding) -#if os(macOS) || targetEnvironment(macCatalyst) + // simple tap to select (or unselect) .contentShape(Rectangle()) - .onTapGesture { selected = element.id } + .onTapGesture { + if selected == element.id { + selected = nil + } else { + selected = element.id + } + } +#if os(macOS) || targetEnvironment(macCatalyst) .onHover { if $0 { hovered = element.id } } //.frame(maxWidth: .infinity) // NOTE this centers the grid item .background(hovered == element.id ? config.hoverColor : Color.clear) diff --git a/Sources/Grid/Internal/GridItemModM.swift b/Sources/Grid/Internal/GridItemModM.swift new file mode 100644 index 0000000..511ad84 --- /dev/null +++ b/Sources/Grid/Internal/GridItemModM.swift @@ -0,0 +1,54 @@ +// +// GridItemModM.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 + +/// Support for multi-select Grid-based rows +struct GridItemModM: ViewModifier + where Element: Identifiable +{ + typealias Config = TablerGridConfig + typealias Hovered = Element.ID? + typealias Selected = Set + + let config: Config + let element: Element + @Binding var hovered: Hovered + @Binding var selected: Selected + + func body(content: Content) -> some View { + content + .padding(config.itemPadding) + + // simple tap to select (or unselect) + .contentShape(Rectangle()) + .onTapGesture { + if selected.contains(element.id) { + selected.remove(element.id) + } else { + selected.insert(element.id) + } + } + +#if os(macOS) || targetEnvironment(macCatalyst) + .onHover { if $0 { hovered = element.id } } + //.frame(maxWidth: .infinity) // NOTE this centers the grid item + .background(hovered == element.id ? config.hoverColor : Color.clear) +#endif + } +} diff --git a/Sources/Grid/TablerGrid1.swift b/Sources/Grid/TablerGrid1.swift index fff1384..2d5f352 100644 --- a/Sources/Grid/TablerGrid1.swift +++ b/Sources/Grid/TablerGrid1.swift @@ -18,7 +18,7 @@ import SwiftUI -/// Grid-based table, with support for single-selection +/// Grid-based table, with support for single-select public struct TablerGrid1: View where Element: Identifiable, Header: View, diff --git a/Sources/Grid/TablerGrid1B.swift b/Sources/Grid/TablerGrid1B.swift index 5d00f67..36aca51 100644 --- a/Sources/Grid/TablerGrid1B.swift +++ b/Sources/Grid/TablerGrid1B.swift @@ -18,7 +18,7 @@ import SwiftUI -/// Grid-based table, with support for single-selection and bound values from RandomAccessCollection +/// Grid-based table, with support for single-select and bound value types public struct TablerGrid1B: View where Element: Identifiable, Header: View, diff --git a/Sources/Grid/TablerGrid1C.swift b/Sources/Grid/TablerGrid1C.swift index 79825dc..4cec261 100644 --- a/Sources/Grid/TablerGrid1C.swift +++ b/Sources/Grid/TablerGrid1C.swift @@ -16,7 +16,6 @@ // limitations under the License. // -import CoreData import SwiftUI /// Grid-based table, with support for reference types diff --git a/Sources/Grid/TablerGridB.swift b/Sources/Grid/TablerGridB.swift index 5849328..3e39539 100644 --- a/Sources/Grid/TablerGridB.swift +++ b/Sources/Grid/TablerGridB.swift @@ -18,7 +18,7 @@ import SwiftUI -/// Grid-based table, with support for bound values from RandomAccessCollection +/// Grid-based table, with support for bound value types public struct TablerGridB: View where Element: Identifiable, Header: View, diff --git a/Sources/Grid/TablerGridC.swift b/Sources/Grid/TablerGridC.swift index 7075600..de7852c 100644 --- a/Sources/Grid/TablerGridC.swift +++ b/Sources/Grid/TablerGridC.swift @@ -16,7 +16,6 @@ // limitations under the License. // -import CoreData import SwiftUI /// Grid-based table, with support for reference types diff --git a/Sources/Grid/TablerGridM.swift b/Sources/Grid/TablerGridM.swift new file mode 100644 index 0000000..67c2983 --- /dev/null +++ b/Sources/Grid/TablerGridM.swift @@ -0,0 +1,212 @@ +// +// TablerGridM.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 multi-select +public struct TablerGridM: View + where Element: Identifiable, + Header: View, + Row: View, + RowBack: View, + RowOver: View, + Results: RandomAccessCollection, + Results.Element == Element +{ + public typealias Config = TablerGridConfig + public typealias Context = TablerContext + public typealias Hovered = Element.ID? + public typealias HeaderContent = (Binding) -> Header + public typealias RowContent = (Element) -> Row + public typealias RowBackground = (Element) -> RowBack + public typealias RowOverlay = (Element) -> RowOver + public typealias Selected = Set + + // MARK: Parameters + + private let config: Config + private let headerContent: HeaderContent + private let rowContent: RowContent + private let rowBackground: RowBackground + private let rowOverlay: RowOverlay + private var results: Results + @Binding private var selected: Selected + + public init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + { + self.config = config + headerContent = header + rowContent = row + self.rowBackground = rowBackground + self.rowOverlay = rowOverlay + self.results = results + _selected = selected + _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.filter(config.filter ?? { _ in true })) { element in + rowContent(element) + .modifier(GridItemModM(config: config, + element: element, + hovered: $hovered, + selected: $selected)) + .background(rowBackground(element)) + .overlay(rowOverlay(element)) + } + } + } +} + +public extension TablerGridM { + // omitting Header + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where Header == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Results, + selected: Binding) + where RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Background + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where RowBack == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Header AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Results, + selected: Binding) + where Header == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header AND Background + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Background AND Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + results: Results, + selected: Binding) + where RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header, Background, AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + results: Results, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } +} diff --git a/Sources/Grid/TablerGridMB.swift b/Sources/Grid/TablerGridMB.swift new file mode 100644 index 0000000..e96eeff --- /dev/null +++ b/Sources/Grid/TablerGridMB.swift @@ -0,0 +1,213 @@ +// +// TablerGridMB.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 multi-select and bound value types +public struct TablerGridMB: View + where Element: Identifiable, + Header: View, + Row: View, + RowBack: View, + RowOver: View, + Results: RandomAccessCollection & MutableCollection, + Results.Element == Element, + Results.Index: Hashable +{ + public typealias Config = TablerGridConfig + public typealias Context = TablerContext + public typealias Hovered = Element.ID? + public typealias HeaderContent = (Binding) -> Header + public typealias RowContent = (Binding) -> Row + public typealias RowBackground = (Element) -> RowBack + public typealias RowOverlay = (Element) -> RowOver + public typealias Selected = Set + + // MARK: Parameters + + private let config: Config + private let headerContent: HeaderContent + private let rowContent: RowContent + private let rowBackground: RowBackground + private let rowOverlay: RowOverlay + @Binding private var results: Results + @Binding private var selected: Selected + + public init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Binding, + selected: Binding) + { + self.config = config + headerContent = header + rowContent = row + self.rowBackground = rowBackground + self.rowOverlay = rowOverlay + _results = results + _selected = selected + _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) { $element in + rowContent($element) + .modifier(GridItemModM(config: config, + element: element, + hovered: $hovered, + selected: $selected)) + .background(rowBackground(element)) + .overlay(rowOverlay(element)) + } + } + } +} + +public extension TablerGridMB { + // omitting Header + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Binding, + selected: Binding) + where Header == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Binding, + selected: Binding) + where RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Background + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Binding, + selected: Binding) + where RowBack == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Header AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Binding, + selected: Binding) + where Header == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header AND Background + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Binding, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Background AND Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + results: Binding, + selected: Binding) + where RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header, Background, AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + results: Binding, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } +} diff --git a/Sources/Grid/TablerGridMC.swift b/Sources/Grid/TablerGridMC.swift new file mode 100644 index 0000000..c8cbdb7 --- /dev/null +++ b/Sources/Grid/TablerGridMC.swift @@ -0,0 +1,215 @@ +// +// TablerGridMC.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 multi-select and reference types +public struct TablerGridMC: View + where Element: Identifiable & ObservableObject, + Header: View, + Row: View, + RowBack: View, + RowOver: View, + Results: RandomAccessCollection, + Results.Element == Element +{ + 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 RowOverlay = (Element) -> RowOver + public typealias Selected = Set + + // MARK: Parameters + + private let config: Config + private let headerContent: HeaderContent + private let rowContent: RowContent + private let rowBackground: RowBackground + private let rowOverlay: RowOverlay + private var results: Results + @Binding private var selected: Selected + + public init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + { + self.config = config + headerContent = header + rowContent = row + self.rowBackground = rowBackground + self.rowOverlay = rowOverlay + self.results = results + _selected = selected + _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(GridItemModM(config: config, + element: rawElem, + hovered: $hovered, + selected: $selected)) + .background(rowBackground(rawElem)) + .overlay(rowOverlay(rawElem)) + } + } + } + } +} + +public extension TablerGridMC { + // omitting Header + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where Header == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Results, + selected: Binding) + where RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Background + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where RowBack == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Header AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Results, + selected: Binding) + where Header == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header AND Background + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Background AND Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + results: Results, + selected: Binding) + where RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header, Background, AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + results: Results, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } +} diff --git a/Sources/List/TablerList1B.swift b/Sources/List/TablerList1B.swift index 1846270..944533d 100644 --- a/Sources/List/TablerList1B.swift +++ b/Sources/List/TablerList1B.swift @@ -18,7 +18,7 @@ import SwiftUI -/// List-based table, with support for single-select and bound values from RandomAccessCollection +/// List-based table, with support for single-select and bound value types public struct TablerList1B: View where Element: Identifiable, Header: View, diff --git a/Sources/List/TablerList1C.swift b/Sources/List/TablerList1C.swift index 71d9a4c..52190b2 100644 --- a/Sources/List/TablerList1C.swift +++ b/Sources/List/TablerList1C.swift @@ -16,10 +16,9 @@ // limitations under the License. // -import CoreData import SwiftUI -/// List-based table, with support for single-selection and reference types +/// List-based table, with support for single-select and reference types public struct TablerList1C: View where Element: Identifiable & ObservableObject, Header: View, diff --git a/Sources/List/TablerListB.swift b/Sources/List/TablerListB.swift index 3787a22..e2979ec 100644 --- a/Sources/List/TablerListB.swift +++ b/Sources/List/TablerListB.swift @@ -18,7 +18,7 @@ import SwiftUI -/// List-based table, with support for bound values from RandomAccessCollection +/// List-based table, with support for bound value types public struct TablerListB: View where Element: Identifiable, Header: View, diff --git a/Sources/List/TablerListC.swift b/Sources/List/TablerListC.swift index d1f7e22..7e49865 100644 --- a/Sources/List/TablerListC.swift +++ b/Sources/List/TablerListC.swift @@ -16,7 +16,6 @@ // limitations under the License. // -import CoreData import SwiftUI /// List-based table, with support for reference types diff --git a/Sources/List/TablerListMB.swift b/Sources/List/TablerListMB.swift index d74c1c4..0669a78 100644 --- a/Sources/List/TablerListMB.swift +++ b/Sources/List/TablerListMB.swift @@ -18,7 +18,7 @@ import SwiftUI -/// List-based table, with support for multi-select and bound values from RandomAccessCollection +/// List-based table, with support for multi-select and bound value types public struct TablerListMB: View where Element: Identifiable, Header: View, diff --git a/Sources/List/TablerListMC.swift b/Sources/List/TablerListMC.swift index 3715051..02b3136 100644 --- a/Sources/List/TablerListMC.swift +++ b/Sources/List/TablerListMC.swift @@ -16,10 +16,9 @@ // limitations under the License. // -import CoreData import SwiftUI -/// List-based table, with support for multi-selection and reference types +/// List-based table, with support for multi-select and reference types public struct TablerListMC: View where Element: Identifiable & ObservableObject, Header: View, diff --git a/Sources/Stack/Internal/StackRowMod1.swift b/Sources/Stack/Internal/StackRowMod1.swift index 2930d37..b991b03 100644 --- a/Sources/Stack/Internal/StackRowMod1.swift +++ b/Sources/Stack/Internal/StackRowMod1.swift @@ -18,6 +18,7 @@ import SwiftUI +/// Support for single-select Stack-based rows struct StackRowMod1: ViewModifier where Element: Identifiable { @@ -34,10 +35,17 @@ where Element: Identifiable content .padding(config.rowPadding) -#if os(macOS) || targetEnvironment(macCatalyst) + // simple tap to select (or unselect) .contentShape(Rectangle()) - .onTapGesture { selected = element.id } + .onTapGesture { + if selected == element.id { + selected = nil + } else { + selected = element.id + } + } +#if os(macOS) || targetEnvironment(macCatalyst) .onHover { if $0 { hovered = element.id } } .background(hovered == element.id ? config.hoverColor : Color.clear) #endif diff --git a/Sources/Stack/Internal/StackRowModM.swift b/Sources/Stack/Internal/StackRowModM.swift new file mode 100644 index 0000000..4d4b1be --- /dev/null +++ b/Sources/Stack/Internal/StackRowModM.swift @@ -0,0 +1,53 @@ +// +// StackRowModM.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 + +/// Support for multi-select Stack-based rows +struct StackRowModM: ViewModifier +where Element: Identifiable +{ + typealias Config = TablerStackConfig + typealias Hovered = Element.ID? + typealias Selected = Set + + let config: Config + let element: Element + @Binding var hovered: Hovered + @Binding var selected: Selected + + func body(content: Content) -> some View { + content + .padding(config.rowPadding) + + // simple tap to select (or unselect) + .contentShape(Rectangle()) + .onTapGesture { + if selected.contains(element.id) { + selected.remove(element.id) + } else { + selected.insert(element.id) + } + } + +#if os(macOS) || targetEnvironment(macCatalyst) + .onHover { if $0 { hovered = element.id } } + .background(hovered == element.id ? config.hoverColor : Color.clear) +#endif + } +} diff --git a/Sources/Stack/TablerStack1B.swift b/Sources/Stack/TablerStack1B.swift index e1d1fc1..73c5917 100644 --- a/Sources/Stack/TablerStack1B.swift +++ b/Sources/Stack/TablerStack1B.swift @@ -18,7 +18,7 @@ import SwiftUI -/// Stack-based table, with support for single-select and bound values from RandomAccessCollection +/// Stack-based table, with support for single-select and bound value types public struct TablerStack1B: View where Element: Identifiable, Header: View, diff --git a/Sources/Stack/TablerStack1C.swift b/Sources/Stack/TablerStack1C.swift index bb34c03..f60d1fb 100644 --- a/Sources/Stack/TablerStack1C.swift +++ b/Sources/Stack/TablerStack1C.swift @@ -16,7 +16,6 @@ // limitations under the License. // -import CoreData import SwiftUI /// Stack-based table, with support for reference types diff --git a/Sources/Stack/TablerStackB.swift b/Sources/Stack/TablerStackB.swift index 6540728..5f874dd 100644 --- a/Sources/Stack/TablerStackB.swift +++ b/Sources/Stack/TablerStackB.swift @@ -18,7 +18,7 @@ import SwiftUI -/// Stack-based table, with support for bound values from RandomAccessCollection +/// Stack-based table, with support for bound value types public struct TablerStackB: View where Element: Identifiable, Header: View, diff --git a/Sources/Stack/TablerStackC.swift b/Sources/Stack/TablerStackC.swift index c7f08cc..7e733d8 100644 --- a/Sources/Stack/TablerStackC.swift +++ b/Sources/Stack/TablerStackC.swift @@ -16,7 +16,6 @@ // limitations under the License. // -import CoreData import SwiftUI /// Stack-based table, with support for reference types diff --git a/Sources/Stack/TablerStackM.swift b/Sources/Stack/TablerStackM.swift new file mode 100644 index 0000000..68d0864 --- /dev/null +++ b/Sources/Stack/TablerStackM.swift @@ -0,0 +1,212 @@ +// +// TablerStackM.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 + +/// Stack-based table, with support for multi-select +public struct TablerStackM: View + where Element: Identifiable, + Header: View, + Row: View, + RowBack: View, + RowOver: View, + Results: RandomAccessCollection, + Results.Element == Element +{ + public typealias Config = TablerStackConfig + public typealias Context = TablerContext + public typealias Hovered = Element.ID? + public typealias HeaderContent = (Binding) -> Header + public typealias RowContent = (Element) -> Row + public typealias RowBackground = (Element) -> RowBack + public typealias RowOverlay = (Element) -> RowOver + public typealias Selected = Set + + // MARK: Parameters + + private let config: Config + private let headerContent: HeaderContent + private let rowContent: RowContent + private let rowBackground: RowBackground + private let rowOverlay: RowOverlay + private var results: Results + @Binding private var selected: Selected + + public init(_ config: Config = .init(), + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + { + self.config = config + headerContent = header + rowContent = row + self.rowBackground = rowBackground + self.rowOverlay = rowOverlay + self.results = results + _selected = selected + _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 { + BaseStack(context: $context, + header: headerContent) { + ForEach(results.filter(config.filter ?? { _ in true })) { element in + rowContent(element) + .modifier(StackRowModM(config: config, + element: element, + hovered: $hovered, + selected: $selected)) + .background(rowBackground(element)) + .overlay(rowOverlay(element)) + } + } + } +} + +public extension TablerStackM { + // omitting Header + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where Header == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Results, + selected: Binding) + where RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Background + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where RowBack == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Header AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Results, + selected: Binding) + where Header == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header AND Background + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Background AND Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + results: Results, + selected: Binding) + where RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header, Background, AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + results: Results, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } +} diff --git a/Sources/Stack/TablerStackMB.swift b/Sources/Stack/TablerStackMB.swift new file mode 100644 index 0000000..84563da --- /dev/null +++ b/Sources/Stack/TablerStackMB.swift @@ -0,0 +1,226 @@ +// +// TablerStackMB.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 + +/// Stack-based table, with support for multi-select and bound value types +public struct TablerStackMB: View + where Element: Identifiable, + Header: View, + Row: View, + RowBack: View, + RowOver: View, + Results: RandomAccessCollection & MutableCollection, + Results.Element == Element, + Results.Index: Hashable +{ + public typealias Config = TablerStackConfig + public typealias Context = TablerContext + public typealias Hovered = Element.ID? + public typealias HeaderContent = (Binding) -> Header + public typealias RowContent = (Binding) -> Row + public typealias RowBackground = (Element) -> RowBack + public typealias RowOverlay = (Element) -> RowOver + public typealias Selected = Set + + // MARK: Parameters + + private let config: Config + private let headerContent: HeaderContent + private let rowContent: RowContent + private let rowBackground: RowBackground + private let rowOverlay: RowOverlay + @Binding private var results: Results + @Binding private var selected: Selected + + public init(_ config: Config = .init(), + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Binding, + selected: Binding) + { + self.config = config + headerContent = header + rowContent = row + self.rowBackground = rowBackground + self.rowOverlay = rowOverlay + _results = results + _selected = selected + _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 { + BaseStack(context: $context, + header: headerContent) { + // TODO: is there a better way to filter bound data source? + if let _filter = config.filter { + ForEach($results) { $element in + if _filter(element) { + row($element) + } + } + } else { + ForEach($results) { $element in + row($element) + } + } + } + } + + private func row(_ element: Binding) -> some View { + rowContent(element) + .modifier(StackRowModM(config: config, + element: element.wrappedValue, + hovered: $hovered, + selected: $selected)) + .background(rowBackground(element.wrappedValue)) + .overlay(rowOverlay(element.wrappedValue)) + } +} + +public extension TablerStackMB { + // omitting Header + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Binding, + selected: Binding) + where Header == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Binding, + selected: Binding) + where RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Background + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Binding, + selected: Binding) + where RowBack == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Header AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Binding, + selected: Binding) + where Header == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header AND Background + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Binding, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Background AND Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + results: Binding, + selected: Binding) + where RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header, Background, AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + results: Binding, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } +} diff --git a/Sources/Stack/TablerStackMC.swift b/Sources/Stack/TablerStackMC.swift new file mode 100644 index 0000000..06ab1da --- /dev/null +++ b/Sources/Stack/TablerStackMC.swift @@ -0,0 +1,215 @@ +// +// TablerStackMC.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 + +/// Stack-based table, with support for multi-select and reference types +public struct TablerStackMC: View + where Element: Identifiable & ObservableObject, + Header: View, + Row: View, + RowBack: View, + RowOver: View, + Results: RandomAccessCollection, + Results.Element == Element +{ + public typealias Config = TablerStackConfig + 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 RowOverlay = (Element) -> RowOver + public typealias Selected = Set + + // MARK: Parameters + + private let config: Config + private let headerContent: HeaderContent + private let rowContent: RowContent + private let rowBackground: RowBackground + private let rowOverlay: RowOverlay + private var results: Results + @Binding private var selected: Selected + + public init(_ config: Config = .init(), + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + { + self.config = config + headerContent = header + rowContent = row + self.rowBackground = rowBackground + self.rowOverlay = rowOverlay + self.results = results + _selected = selected + _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 { + BaseStack(context: $context, + header: headerContent) { + ForEach(results) { rawElem in + ObservableHolder(element: rawElem) { obsElem in + rowContent(obsElem) + .modifier(StackRowModM(config: config, + element: rawElem, + hovered: $hovered, + selected: $selected)) + .background(rowBackground(rawElem)) + .overlay(rowOverlay(rawElem)) + } + } + } + } +} + +public extension TablerStackMC { + // omitting Header + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where Header == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Results, + selected: Binding) + where RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Background + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where RowBack == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Header AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowBackground: @escaping RowBackground, + results: Results, + selected: Binding) + where Header == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: rowBackground, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header AND Background + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + @ViewBuilder rowOverlay: @escaping RowOverlay, + results: Results, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: rowOverlay, + results: results, + selected: selected) + } + + // omitting Background AND Overlay + init(_ config: Config, + @ViewBuilder header: @escaping HeaderContent, + @ViewBuilder row: @escaping RowContent, + results: Results, + selected: Binding) + where RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: header, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } + + // omitting Header, Background, AND Overlay + init(_ config: Config, + @ViewBuilder row: @escaping RowContent, + results: Results, + selected: Binding) + where Header == EmptyView, RowBack == EmptyView, RowOver == EmptyView + { + self.init(config, + header: { _ in EmptyView() }, + row: row, + rowBackground: { _ in EmptyView() }, + rowOverlay: { _ in EmptyView() }, + results: results, + selected: selected) + } +}