Skip to content

Instantly share code, notes, and snippets.

@michaelevensen
Last active January 18, 2023 21:15
Show Gist options
  • Save michaelevensen/3488f90209f95eca794e15fba0a1f850 to your computer and use it in GitHub Desktop.
Save michaelevensen/3488f90209f95eca794e15fba0a1f850 to your computer and use it in GitHub Desktop.
An example of data edit flow in SwiftUI. Using Apple's examples.
//
// 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()
}
}
@michaelevensen
Copy link
Author

michaelevensen commented Dec 22, 2022

You can also update the model values by binding direclty to them (this would simplify the init for EditView), but for some optional values you can't. This doesn't work great for for instance DatePicker so thus I'm here duplicating the data into a new MutableData struct to handle the internal state values for EditView.

You can however support optional picker values like so:

Picker("Topping", selection: $donut.topping) {
                Section {
                    Text("None")
                        .tag(nil as Donut.Topping?)
                }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment