swiftui - Restore values of a form when cancelling the update - Stack Overflow

admin2025-04-17  2

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)
    }
}

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)
    }
}
Share Improve this question edited Jan 31 at 9:11 Joakim Danielson 52.3k5 gold badges33 silver badges71 bronze badges asked Jan 31 at 6:49 EdwardEdward 458 bronze badges 1
  • @JoakimDanielson No that doesn't work, I guess because the view can be rebuilt at any time so the init() can be called again when the values have already been modified so the initialValues will take the modified ones. – Edward Commented Jan 31 at 8:07
Add a comment  | 

3 Answers 3

Reset to default 2

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()
    }
}

I would just bind the text fields to the initialTitle and initialAuthor state variables, then use them to update the book on save. Then you don't have to worry about rolling anything back because nothing changed in the first place. Something like this:


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: $initialTitle, axis: .vertical)
            TextField("Author", text: $initialAuthor, axis: .vertical)
        }
        .toolbar {
            ToolbarItem(placement: .confirmationAction) {
                Button("Save") {
                    book.title = self.initialTitle
                    book.author = self.initialAuthor
                    dismiss()
                }
            }
            ToolbarItem(placement: .cancellationAction) {
                Button("Cancel") {
                    dismiss()
                }
            }
        }
    }
    
    init(book: Book) {
        self.book = book
        self.initialTitle = book.title
        self.initialAuthor = book.author
    }
}

You don't have to pass a binding. Just pass the object and load into local states. Then, on save, update the original object:

import SwiftUI
import SwiftData

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

//Main view
struct BookList: View {
    
    //Sample data
    let books: [Book] = [
        Book(title: "Book 1", author: "Author 1"),
        Book(title: "Book 2", author: "Author 2"),
        Book(title: "Book 3", author: "Author 2"),
    ]
    
    //Body
    var body: some View {
        NavigationStack {
            List(books) { book in
                NavigationLink {
                    BookDetail(book: book)
                } label: {
                    VStack(alignment: .leading) {
                        Text(book.title)
                        Text("By: \(book.author)")
                            .foregroundStyle(.secondary)
                            .font(.subheadline)
                    }
                }
            }
            .navigationTitle("Book List")
        }
    }
}

//Child view
struct BookDetail: View {
    
    //Parameters
    let book: Book
    
    //State values
    @State private var bookTitle: String = ""
    @State private var bookAuthor: String = ""
    
    //Environment values
    @Environment(\.dismiss) private var dismiss
    @Environment(\.modelContext) private var context
    
    //Body
    var body: some View {
        
        //Form
        Form {
            Section("Edit book details") {
                
                //Edit fields
                TextField("Title", text: $bookTitle, axis: .vertical)
                TextField("Author", text: $bookAuthor , axis: .vertical)
            }
        }
        .navigationTitle("Edit book")
        .navigationBarBackButtonHidden()
        .toolbar {
            ToolbarItem(placement: .confirmationAction) {
                Button("Save") {
                    save()
                    dismiss()
                }
            }
            ToolbarItem(placement: .cancellationAction) {
                Button("Cancel") {
                    dismiss()
                }
            }
        }
        .onAppear { // <- load properties into local states
            bookTitle = book.title
            bookAuthor = book.author
        }
    }
    
    //Save function
    private func save() {
        book.title = bookTitle // <- save state/form values back to object
        book.author = bookAuthor // <- save state/form values back to object
    }

}

//Preview
#Preview {
    NavigationStack {
        BookList()
            .modelContainer(for: Book.self)
    }
}
转载请注明原文地址:http://anycun.com/QandA/1744877488a88891.html