Skip to main content

How to discard changes to a SwiftData object

About 2 minSwiftArticle(s)bloghackingwithswift.comcrashcourseswiftswiftdataxcodeappstore

How to discard changes to a SwiftData object 관련

SwiftData by Example

Back to Home

How to discard changes to a SwiftData object | SwiftData by Example

How to discard changes to a SwiftData object

Updated for Xcode 15

SwiftData makes it easy to pass an object between various parts of your app, and have changes made in one place automatically be reflected elsewhere: just create an editing view with your model object as @Bindable, and SwiftData takes care of the rest.

However, it's a little trickier when you want the user to be able to save or discard their changes in an editing view, because SwiftData will automatically synchronize changes as soon as they happen.

The problem happens because SwiftData doesn't have the concept of a child context, so we can't make some isolated changes then merge them upwards only when needed. Instead, the best solution I've found is to create a peer context from the same container, pass in your model object's ID to your detail view, then load it there using the new context with autosave disabled.

Tips

This solution works, but I've found it causes SwiftUI to screw up its list deselection animation when returning to the parent view – the row you were editing stays highlighted. I have yet to find a workaround for this, so if you have a better idea please let me know!

Let me walk you the solution I use. First, here's an example data model

@Model
class Issue {
    var name: String

    init(name: String) {
        self.name = name
    }
}

Second, we need a ContentView that can show issue data and navigate to edit a single Issue when it’s selected.

Remember, we don't pass the actual issue into the editing view, because it will exist on our default model context with autosave enabled. Instead, we pass in our existing container along with the Issue identifier, so we can create a fresh context in the editing view and load the Issue object there.

This is a process sometimes called rehydrating the object: we can’t share a single model object across two model contexts, so instead we pass the identifier and load it separately in our new context.

Here’s an example ContentView doing that:

struct ContentView: View {
    @Environment(\.modelContext) var modelContext
    @Query(sort: \Issue.name) var issues: [Issue]

    var body: some View {
        NavigationStack {
            List(issues) { issue in
                NavigationLink(value: issue) {
                    Text(issue.name)
                }
            }
            .navigationDestination(for: Issue.self) { issue in
                EditingView(issueID: issue.id, in: modelContext.container)
            }
            .navigationTitle("Discardable Editing")
        }
    }
}

Now for the important part: an EditingView that accepts an object ID along with our shared model container. It will then create its own local model context that disables autosave, loading the editing object using that context.

struct EditingView: View {
    @Environment(\.dismiss) var dismiss
    @Bindable var issue: Issue

    var modelContext: ModelContext

    init(issueID: PersistentIdentifier, in container: ModelContainer) {
        modelContext = ModelContext(container)
        modelContext.autosaveEnabled = false
        issue = modelContext.model(for: issueID) as? Issue ?? Issue(name: "New Issue")
    }

    var body: some View {
        Form {
            TextField("Edit the name", text: $issue.name)
        }
        .toolbar {
            Button("Discard") {
                dismiss()
            }

            Button("Save") {
                try? modelContext.save()
                dismiss()
            }
        }
    }
}

As you can see, that exits the view without saving when "Discard" is pressed, which means all the local edits to the object aren't synchronized with the original context – they just get tossed away, because the local context is discarded without saving.

In my various projects I've found this to be the simplest way of handling discardable editing of SwiftData objects, but if you have a better solution I'd love to hear it!


이찬희 (MarkiiimarK)
Never Stop Learning.