I have a problem when updating the Bool on my SwiftData model, at first it's changing. But if I minimize / close the app and reopen again, the value goes back to the first time the record was added (false)
import SwiftUI
import SwiftData
struct CategoryList: View {
@Environment(\.modelContext) private var context
@Query var categories: [Category]
@State private var isFormSheetPresented = false // State for presenting the Add Category form
var body: some View {
List {
// Active Categories
Section(header: Text("Active Categories")
.font(.headline)
.foregroundColor(.black)) { // Header with black text
ForEach(activeCategories) { category in
HStack {
Text(category.name)
.foregroundColor(.darkSpringGreen)
Spacer()
if !category.isDefault {
// Show a delete button for non-default categories
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture(perform: {
deleteReactivateCategory(category, isDelete: true)
})
}
}
}
}
.listRowBackground(Color.white) // Set white background for the section
// Deleted Categories
Section(header: Text("Deleted Categories")
.font(.headline)
.foregroundColor(.black)) { // Header with black text
ForEach(deletedCategories) { category in
HStack {
Text(category.name)
.foregroundColor(.lightSilver) // Indicate that it's deleted
Spacer()
Image(systemName: "arrow.trianglehead.counterclockwise.rotate.90")
.foregroundColor(.red)
.onTapGesture(perform: {
deleteReactivateCategory(category, isDelete: false)
})
}
}
}
.listRowBackground(Color.white) // Set white background for the section
}
.listStyle(PlainListStyle()) // Use plain list style to remove default styling
.navigationTitle("Categories") // Title is centered by default
.navigationBarTitleDisplayMode(.inline) // Make title not large
.navigationBarItems(trailing: Button(action: { isFormSheetPresented = true // Present the form
}) {
Text("Add")
})
.sheet(isPresented: $isFormSheetPresented) {
CategoryForm(isEdit: false, dismissAction: {
isFormSheetPresented = false // Dismiss action for the form
}, addAction: { newCategoryName in
let newCategory = Category(name: newCategoryName, isDefault: false, isDeleted: false)
let swifDataService = SwiftDataService(context: context)
do {
try swifDataService.createCategory(newCategory: newCategory)
isFormSheetPresented = false
} catch {
print("-- Cateogy failed to add \(newCategoryName)")
}
}).presentationDetents([.medium])
}
}
// Computed properties to separate active and deleted categories
private var activeCategories: [Category] {
categories
.filter { !$0.isDeleted } // Filter out deleted categories
.sorted { (first, second) -> Bool in
// Check if either category is named "Others"
if first.name == "Others" {
return false // "Others" should come last
}
if second.name == "Others" {
return true // "Others" should come last
}
// Otherwise, sort alphabetically
return first.name < second.name
}
}
private var deletedCategories: [Category] {
categories.filter { $0.isDeleted }
}
private func deleteReactivateCategory(_ categoryRecord: Category, isDelete: Bool) {
let updateCategories = categories
if let index = updateCategories.firstIndex(where: { $0.id == categoryRecord.id }) {
updateCategories[index].isDeleted = true
updateCategories[index].name = "Test Changing the name"
}
}
}
and here is the model
import Foundation
import FirebaseFirestore
import SwiftData
@Model
class Category: Codable, Identifiable {
@Attribute(.unique) var id: String
var name: String
var isDefault: Bool
var isDeleted: Bool
// Custom CodingKeys to map property names for encoding/decoding
enum CodingKeys: String, CodingKey {
case id, name, isDefault, isDeleted
}
// Required initializer to decode data
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
isDefault = try container.decode(Bool.self, forKey: .isDefault)
isDeleted = try container.decode(Bool.self, forKey: .isDeleted)
}
// Encode function to encode data
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(isDefault, forKey: .isDefault)
try container.encode(isDeleted, forKey: .isDeleted)
}
// Initializer for creating new instances of Category
init(id: UUID = UUID(), name: String, isDefault: Bool, isDeleted: Bool) {
self.id = id.uuidString
self.name = name
self.isDefault = isDefault
self.isDeleted = isDeleted
}
}
The things that confuse me are, why when I change the String, it's persisted and saved even I minimize or close the apps but not the isDeleted
?
This is a case of bad luck I would say, your property isDeleted
already exists in PersistentModel
and is added automatically by the @Model
macro when the code is compiled.
So what happens is that when you updated isDeleted
it was your own property that got changed (I confirmed this by looking in the sqlite database) but in your computed properties where you used filter
it was the other property from PersistentModel
that was read. And since you don't really delete the object that property is always false.
I tested this by renaming your property to isDeleted2
and then everything worked as expected.
So in short the solution is to rename the property to something else
var isCategoryDeleted: Bool