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