swiftswiftuiswiftdata

Update boolean value not saved after minimizing the app


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?


Solution

  • 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