Last active
January 18, 2023 21:15
-
-
Save michaelevensen/3488f90209f95eca794e15fba0a1f850 to your computer and use it in GitHub Desktop.
An example of data edit flow in SwiftUI. Using Apple's examples.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// BindingTest.swift | |
// MutatingState | |
// | |
// Created by Michael Nino Evensen on 21/12/2022. | |
// | |
import SwiftUI | |
struct Object: Identifiable, Hashable { | |
var id = UUID() | |
var value: Int | |
var string: String? | |
var date: Date? | |
init(value: Int = 1, string: String? = nil, date: Date? = nil) { | |
self.value = value | |
self.string = string | |
self.date = date | |
} | |
struct MutableData { | |
var value: Int = 1 | |
var string: String = "" | |
var date: Date = .now | |
} | |
var mutableData: MutableData { | |
return MutableData(value: value, string: string ?? "", date: date ?? .now) | |
} | |
mutating func update(with data: MutableData) -> Self { | |
self.value = data.value | |
self.string = data.string | |
self.date = data.date | |
return self | |
} | |
} | |
struct BindingTest: View { | |
@State private var showNewItemSheet: Bool = false | |
@State private var items = [ | |
Object(value: 2, string: "Lorem ipsum", date: .now), | |
Object(string: "Test"), | |
Object(value: 12, string: "Nada", date: .now), | |
Object(value: 1, string: "Blabla") | |
] | |
// New | |
@State var new = Object() | |
var body: some View { | |
NavigationStack { | |
List { | |
ForEach(items) { item in | |
NavigationLink(value: item.id) { | |
ItemView(obj: item) | |
} | |
} | |
.onMove(perform: move) | |
.onDelete(perform: delete) | |
} | |
.navigationTitle("Items") | |
.navigationDestination(for: Object.ID.self) { id in | |
DetailView(obj: self.objBinding(for: id)) | |
} | |
.safeAreaInset(edge: .bottom) { | |
Button("+ Add new") { | |
showNewItemSheet = true | |
} | |
.buttonStyle(.borderedProminent) | |
} | |
} | |
.sheet(isPresented: $showNewItemSheet) { | |
// New item | |
EditView(initialValue: $new, didSave: { newItem in | |
self.items.append(newItem) | |
// Reset | |
new = Object() | |
}) | |
} | |
} | |
// Delete | |
func delete(at offsets: IndexSet) { | |
self.items.remove(atOffsets: offsets) | |
} | |
// Move | |
private func move(from source: IndexSet, to destination: Int) { | |
self.items.move(fromOffsets: source, toOffset: destination) | |
} | |
public func objBinding(for id: Object.ID) -> Binding<Object> { | |
Binding<Object> { | |
guard let index = self.items.firstIndex(where: { $0.id == id }) else { | |
fatalError() | |
} | |
return self.items[index] | |
} set: { newValue in | |
guard let index = self.items.firstIndex(where: { $0.id == id }) else { | |
fatalError() | |
} | |
self.items[index] = newValue | |
} | |
} | |
} | |
struct ItemView: View { | |
// @Binding var obj: Object | |
var obj: Object | |
var body: some View { | |
HStack { | |
VStack(alignment: .leading) { | |
if let title = obj.string { | |
Text(title) | |
} | |
if let date = obj.date { | |
Text(date.formatted(.dateTime.day().month())) | |
.foregroundColor(.secondary) | |
.font(.caption2) | |
} | |
} | |
Spacer() | |
Text(String(describing: obj.value.formatted(.number))) | |
.foregroundColor(.secondary) | |
} | |
} | |
} | |
struct DetailView: View { | |
@Binding var obj: Object | |
@State private var showEditView: Bool = false | |
var body: some View { | |
VStack(alignment: .leading) { | |
VStack(alignment: .leading) { | |
Text(obj.string ?? "") | |
.font(.largeTitle) | |
if let date = obj.date { | |
Text(date.formatted(.dateTime.day().month())) | |
.font(.subheadline) | |
.foregroundColor(.secondary) | |
} | |
} | |
Spacer() | |
Text(String(describing: obj.value.formatted(.number))) | |
.foregroundColor(.secondary) | |
} | |
.safeAreaInset(edge: .bottom) { | |
Button("Edit") { showEditView = true } | |
.buttonStyle(.borderedProminent) | |
} | |
.sheet(isPresented: $showEditView) { | |
EditView(initialValue: $obj) | |
} | |
} | |
} | |
struct EditView: View { | |
@Environment(\.dismiss) private var dismiss | |
@Binding var obj: Object | |
@State private var didSpecifyDate: Bool = false | |
// Mutable local state | |
@State private var mutable = Object.MutableData() | |
let didSave: ((Object) -> Void)? | |
init( | |
initialValue: Binding<Object>, | |
didSave: ((Object) -> Void)? = { _ in } | |
) { | |
self._obj = initialValue | |
self.didSave = didSave | |
self._mutable = State(initialValue: initialValue.wrappedValue.mutableData) | |
if let _ = initialValue.wrappedValue.date { | |
self._didSpecifyDate = State(initialValue: true) | |
} | |
} | |
var body: some View { | |
VStack { | |
Text(String(describing: self.mutable)) | |
TextField("String", text: $mutable.string) | |
Toggle(isOn: $didSpecifyDate) { | |
Label("Has Date", systemImage: "calendar") | |
} | |
if didSpecifyDate { | |
DatePicker("Date", selection: $mutable.date, displayedComponents: .date) | |
} | |
Stepper(mutable.value.formatted(.number), value: $mutable.value, in: 0...10) | |
Button("Save") { save() } | |
.buttonStyle(.borderedProminent) | |
} | |
.padding() | |
} | |
private func save() { | |
var updated = self.obj.update(with: mutable) | |
// Remove date if unspecified | |
if didSpecifyDate == false { | |
self.obj.date = nil | |
updated.date = nil | |
} | |
didSave?(updated) | |
dismiss() | |
} | |
} | |
struct BindingTest_Previews: PreviewProvider { | |
static var previews: some View { | |
BindingTest() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You can also update the model values by binding direclty to them (this would simplify the
init
forEditView
), but for some optional values you can't. This doesn't work great for for instanceDatePicker
so thus I'm here duplicating the data into a newMutableData
struct to handle the internal state values forEditView
.You can however support optional picker values like so: