Skip to content

Commit 3472d52

Browse files
authored
Various additions and refactoring to support both Random Access Collections and Core Data sources (#9)
1 parent 409b8e0 commit 3472d52

9 files changed

+245
-201
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ If this validation is used, the user will not be able to save changes until it r
217217
## See Also
218218

219219
* [DetailerDemo](https://github.com/openalloc/DetailerDemo) - the demonstration app for this library
220+
* [TablerCoreDemo](https://github.com/openalloc/TablerCoreDemo) - the demonstration app for this library, for Core Data sources
220221

221222
Swift open-source libraries (by the same author):
222223

Sources/DetailerConfig.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ import SwiftUI
2121
public enum DetailerConfigDefaults {
2222

2323
#if os(macOS)
24-
public static let detailerDefaultMinWidth: CGFloat = 300
24+
public static let minWidth: CGFloat = 300
2525
#elseif os(iOS)
26-
public static let detailerDefaultMinWidth: CGFloat = 0
26+
public static let minWidth: CGFloat = 0
2727
#endif
2828

29-
public static let defaultValidateIndicator: (Bool) -> AnyView = { AnyView(
29+
public static let validateIndicator: (Bool) -> AnyView = { AnyView(
3030
Image(systemName: "exclamationmark.triangle")
3131
.font(.title2)
3232
.backport.symbolRenderingMode()
@@ -61,15 +61,15 @@ public struct DetailerConfig<Element>
6161
public let titler: Titler
6262
public let validateIndicator: ValidateIndicator
6363

64-
public init(minWidth: CGFloat = DetailerConfigDefaults.detailerDefaultMinWidth,
64+
public init(minWidth: CGFloat = DetailerConfigDefaults.minWidth,
6565
canEdit: CanEdit? = nil,
6666
canDelete: @escaping CanDelete = { _ in true },
6767
onDelete: OnDelete? = nil,
6868
onValidate: @escaping OnValidate = { _, _ in [] },
6969
onSave: OnSave? = nil,
7070
onCancel: @escaping OnCancel = { _, _ in },
7171
titler: @escaping Titler,
72-
validateIndicator: @escaping ValidateIndicator = DetailerConfigDefaults.defaultValidateIndicator)
72+
validateIndicator: @escaping ValidateIndicator = DetailerConfigDefaults.validateIndicator)
7373
{
7474
self.minWidth = minWidth
7575
self.canEdit = canEdit

Sources/Internal/EditDetail.swift renamed to Sources/Internal/EditDetailBase.swift

+36-39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// EditDetail.swift
2+
// SwiftUIView.swift
33
//
44
// Copyright 2022 FlowAllocator LLC
55
//
@@ -18,37 +18,34 @@
1818

1919
import SwiftUI
2020

21-
struct EditDetail<Element, Detail>: View
22-
where Element: Identifiable,
23-
Detail: View
21+
struct EditDetailBase<Element, Detail>: View
22+
where Element: Identifiable,
23+
Detail: View
2424
{
2525
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
26-
27-
typealias DetailContent = (DetailerContext<Element>, Binding<Element>) -> Detail
26+
27+
typealias DetailContent = (DetailerContext<Element>) -> Detail
2828
typealias Validate<T> = (KeyPath<Element, T>) -> Void
29-
29+
3030
// MARK: Parameters
31-
32-
// NOTE `element` not a binding, because we don't want to change data live
33-
// NOTE `isAdd` will be set to `false` on dismissal of sheet
34-
31+
3532
var config: DetailerConfig<Element>
36-
@State var element: Element
33+
var element: Element
3734
@Binding var isAdd: Bool
3835
var detailContent: DetailContent
39-
36+
4037
// MARK: Locals
41-
38+
4239
@State private var invalidFields: Set<AnyKeyPath> = Set()
4340
@State private var showValidationAlert = false
4441
@State private var alertMessage: String? = nil
45-
42+
4643
private var context: DetailerContext<Element> {
4744
DetailerContext<Element>(config: config,
4845
onValidate: validateAction,
4946
isAdd: isAdd)
5047
}
51-
48+
5249
private var isDeleteAvailable: Bool {
5350
config.onDelete != nil
5451
}
@@ -60,29 +57,29 @@ struct EditDetail<Element, Detail>: View
6057
private var isSaveAvailable: Bool {
6158
config.onSave != nil
6259
}
63-
60+
6461
private var canSave: Bool {
6562
isSaveAvailable && invalidFields.isEmpty
6663
}
67-
64+
6865
// MARK: Views
69-
66+
7067
var body: some View {
7168
VStack(alignment: .leading) { // .leading needed to keep title from centering
72-
#if os(macOS)
69+
#if os(macOS)
7370
Text(config.titler(element)).font(.largeTitle)
74-
#endif
71+
#endif
7572
// this is where the user will typically declare a Form or VStack
76-
detailContent(context, $element)
73+
detailContent(context)
7774
// .animation(.default)
7875
}
7976
.alert(isPresented: $showValidationAlert) {
8077
Alert(title: Text("Validation Failure"),
8178
message: Text(alertMessage ?? "Requires valid entry before save."))
8279
}
83-
#if os(macOS)
80+
#if os(macOS)
8481
.padding()
85-
#endif
82+
#endif
8683
.toolbar {
8784
ToolbarItem(placement: .destructiveAction) {
8885
Button(action: deleteAction) {
@@ -92,7 +89,7 @@ struct EditDetail<Element, Detail>: View
9289
.opacity(isDeleteAvailable ? 1 : 0)
9390
.disabled(!canDelete)
9491
}
95-
92+
9693
ToolbarItem(placement: .cancellationAction) {
9794
Button(action: cancelAction) {
9895
Text("Cancel")
@@ -107,18 +104,18 @@ struct EditDetail<Element, Detail>: View
107104
.disabled(!canSave)
108105
}
109106
}
110-
#if os(macOS)
107+
#if os(macOS)
111108
// NOTE on macOS, this seems to be needed to avoid excessive height
112109
.frame(minWidth: config.minWidth, maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
113-
#endif
114-
115-
#if os(iOS) || targetEnvironment(macCatalyst)
110+
#endif
111+
112+
#if os(iOS) || targetEnvironment(macCatalyst)
116113
.navigationTitle(config.titler(element))
117-
#endif
114+
#endif
118115
}
119-
116+
120117
// MARK: Action Handlers
121-
118+
122119
// NOTE: should be invoked via async to avoid updating the state during view render
123120
private func validateAction(_ anyKeyPath: AnyKeyPath, _ result: Bool) {
124121
if result {
@@ -133,40 +130,40 @@ struct EditDetail<Element, Detail>: View
133130
}
134131
}
135132
}
136-
133+
137134
private func saveAction() {
138135
guard let _onSave = config.onSave else { return }
139-
136+
140137
// display any validation changes
141138
let messages = config.onValidate(context, element)
142139
if messages.count > 0 {
143140
alertMessage = config.onValidate(context, element).joined(separator: "\n\n")
144141
showValidationAlert = true
145142
return
146143
}
147-
144+
148145
let invalidCount = invalidFields.count
149146
if invalidCount > 0 {
150147
alertMessage = "\(invalidCount) field(s) require valid values before you can save."
151148
showValidationAlert = true
152149
return
153150
}
154-
151+
155152
_onSave(context, element)
156153
dismissAction()
157154
}
158-
155+
159156
private func deleteAction() {
160157
guard let _onDelete = config.onDelete else { return }
161158
_onDelete(element.id)
162159
dismissAction()
163160
}
164-
161+
165162
private func cancelAction() {
166163
config.onCancel(context, element)
167164
dismissAction()
168165
}
169-
166+
170167
private func dismissAction() {
171168
isAdd = false
172169
presentationMode.wrappedValue.dismiss()

Sources/Internal/EditDetailC.swift

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// EditDetailC.swift
3+
//
4+
// Copyright 2022 FlowAllocator LLC
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
import SwiftUI
20+
21+
/// Core Data support
22+
struct EditDetailC<Element, Detail>: View
23+
where Element: Identifiable & ObservableObject,
24+
Detail: View
25+
{
26+
typealias ProjectedValue = ObservedObject<Element>.Wrapper
27+
typealias DetailContent = (DetailerContext<Element>, ProjectedValue) -> Detail
28+
29+
var config: DetailerConfig<Element>
30+
@ObservedObject var element: Element
31+
@Binding var isAdd: Bool
32+
var detailContent: DetailContent
33+
34+
var body: some View {
35+
EditDetailBase(config: config,
36+
element: element,
37+
isAdd: $isAdd) { context in
38+
detailContent(context, $element)
39+
}
40+
}
41+
}

Sources/Internal/EditDetailR.swift

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// EditDetail.swift
3+
//
4+
// Copyright 2022 FlowAllocator LLC
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
import SwiftUI
20+
21+
/// RandomAccess support
22+
struct EditDetailR<Element, Detail>: View
23+
where Element: Identifiable,
24+
Detail: View
25+
{
26+
typealias BoundValue = Binding<Element>
27+
typealias DetailContent = (DetailerContext<Element>, BoundValue) -> Detail
28+
29+
var config: DetailerConfig<Element>
30+
@State var element: Element
31+
@Binding var isAdd: Bool
32+
var detailContent: DetailContent
33+
34+
var body: some View {
35+
EditDetailBase(config: config,
36+
element: element,
37+
isAdd: $isAdd) { context in
38+
detailContent(context, $element)
39+
}
40+
}
41+
}
42+
43+
44+
45+

Sources/Internal/EditDetailer.swift

-60
This file was deleted.

0 commit comments

Comments
 (0)