Search code examples
swiftuiswiftdata

Restore values of a form when cancelling the update


I have the following code which does what I want, "Cancel" restores the initial values of the book. But is there a better way to do this? Having to save individually each field of a book (there could be a lot of them) doesn't look good. Using the whole book as a @State doesn't work, I get the compilation error Cannot assign to property: 'self' is immutable when trying to update the whole book at once in restoreBook(). If I understand this correctly, I need the initialValues to be @State so they are kept even if the view is rebuilt.

import SwiftUI
import SwiftData

@Model
class Book {
    var title: String
    var author: String
    
    init(title: String, author: String) {
        self.title = title
        self.author = author
    }
}

struct BookDetail: View {
    @Bindable var book: Book
    @State private var initialTitle: String = ""
    @State private var initialAuthor: String = ""
    
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        Form {
            TextField("Title", text: $book.title, axis: .vertical)
            TextField("Author", text: $book.author, axis: .vertical)
        }
        .toolbar {
            ToolbarItem(placement: .confirmationAction) {
                Button("Save") {
                    dismiss()
                }
            }
            ToolbarItem(placement: .cancellationAction) {
                Button("Cancel") {
                    restoreBook()
                    dismiss()
                }
            }
        }
    }
    
    func restoreBook() {
        book.title = initialTitle
        book.author = initialAuthor
    }
    
    init(book: Book) {
        self.book = book
        self._initialTitle = State(wrappedValue: book.title)
        self._initialAuthor = State(wrappedValue: book.author)
    }
}

Solution

  • On approach to solve this that focuses more on SwiftData than SwiftUI is to work with a local ModelContext object in the view and make use of the possibilities to manually save or rollback any changes.

    First change the property declarations

    @Environment(\.modelContext) private var modelContext
    @State private var localContext: ModelContext?
    @State var book: Book
    

    Note that depending on how your ModelContainer is declared you wont' need the modelContext property if you can access the model container in a global way but here I assume you can't.

    Then we set things up in onAppear where we create the new local context and also load the Book object we are going to work with from this local context since we don't want to change anything in the main context.

    .onAppear {
        self.localContext = ModelContext(modelContext.container)
        localContext?.autosaveEnabled = false
        self.book = localContext?.model(for: book.id) as! Book //Replace book from main context with one from the local context
    }
    

    Then we need to either change or rollback the changes in the buttons

    ToolbarItem(placement: .confirmationAction) {
        Button("Save") {
            try? localContext?.save()
            dismiss()
        }
    }
    ToolbarItem(placement: .cancellationAction) {
        Button("Cancel") {
            localContext?.rollback()
            dismiss()
        }
    }