Core Data items in List view don’t delete properly

  Kiến thức lập trình

In my app, I have a List view called LiftEventsView that displays lift events. Lift events are stored in Core Data as a LiftEvent.

When I swipe-to-delete a lift event, it doesn’t immediately disappear from the list. The first time I swipe, the row remains visible, but the ‘eventReps’, ‘eventWeight’, and ‘formattedOneRepMax’ values disappear. If I swipe a second time, the LiftEvent is deleted.

One lift event in the list:

enter image description here

After the first swipe:

enter image description here

After the second swipe:

enter image description here

I’m learning SwiftUI from a Hacking with Swift+ tutorial and the relevant parts of my code are identical as far as I can tell. I’ve been debugging for 2 days, checking and rechecking the code, but can’t figure it out.

struct LiftEventsView: View {
    @EnvironmentObject var dataController: DataController

    let viewModel = LiftEventRowViewModel(calculator: Calculator())

    var body: some View {
        List {
            ForEach(dataController.liftEventsForSelectedFilter()) { liftEvent in
                LiftEventRow(liftEvent: liftEvent, viewModel: viewModel)
            }
            .onDelete(perform: delete)
        }
        .navigationTitle("Lift Log")
    }

    func delete(_ offsets: IndexSet) {
        var events = dataController.liftEventsForSelectedFilter()

        Logger.liftEvents.debug(">>>User swiped to delete")
        Logger.coreData.debug("Events before deletion: (events)")

        for offset in offsets {
            let item = events[offset]
            dataController.delete(item)
        }

        Logger.coreData.debug("Events after deletion: (events)")
    }
}
struct LiftEventRow: View {
    @EnvironmentObject var dataController: DataController
    @ObservedObject var liftEvent: LiftEvent

    let viewModel: LiftEventRowViewModel

    var calculator = Calculator()

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(liftEvent.eventLocalizedLiftName)
                    .font(.system(size: 18, weight: .semibold))
                    .foregroundColor(.black)

                Text("(liftEvent.eventReps) reps @ (liftEvent.eventWeight)" )
            }

            Spacer()

            VStack(alignment: .trailing) {
                if let weight = Double(liftEvent.eventWeight),
                   let reps = Int(liftEvent.eventReps) {
                    let formattedOneRepMax = viewModel.formattedOneRepMax(weight: weight, reps: reps, formulaName: liftEvent.eventFormulaName)

                    Text(formattedOneRepMax)
                        .font(.system(size: 18, weight: .semibold))
                        .foregroundColor(.black)
                }

                Text(liftEvent.eventFormulaName)
            }
        }
    }
}
class DataController: ObservableObject {
    let container: NSPersistentContainer

    @Published var selectedLiftEvent: LiftEvent?

    private var saveTask: Task<Void, Error>?

    // MARK: - Initialization

    init(inMemory: Bool = false) {

        container = NSPersistentContainer(name: "liftLog")

        if inMemory {
            container.persistentStoreDescriptions.first?.url = URL(filePath: "dev/null")
        }

        container.loadPersistentStores { storeDescription, error in
            if let error {
                fatalError("Fatal error loading store: (error.localizedDescription)")
            }
        }

        let primer = CoreDataPrimer(managedObjectContext: self.container.viewContext)
        primer.seedCoreData()

        newLiftEvent()
    }

    func count<T>(for fetchRequest: NSFetchRequest<T>) -> Int {
        (try? container.viewContext.count(for: fetchRequest)) ?? 0
    }
    
    /// Creates a new LiftEvent and assigns it to @Published var selectedLiftEvent
    func newLiftEvent() {
        let liftEvent = LiftEvent(context: container.viewContext)
        liftEvent.uuid = NSUUID().uuidString
        liftEvent.date = .now
        liftEvent.formula = defaultFormula()
        liftEvent.lift = defaultLift()
        liftEvent.weightUnit = UserDefaults.unitsNotation().weightUnit


        self.selectedLiftEvent = liftEvent

        Logger.coreData.info("New lift event (liftEvent.description) created")
        Logger.coreData.info("AFter new event created, selectedLiftEvent is (self.selectedLiftEvent)")
    }

    /// Saves our Core Data context if there are changes. This silently ignores
    /// any errors caused by saving, but this should be fine because
    /// all our attributes are optional.
    func save() {
        saveTask?.cancel()

        if container.viewContext.hasChanges {
            try? container.viewContext.save()

            Logger.coreData.info("Context saved")
        }
    }

    func delete(_ object: NSManagedObject) {
        objectWillChange.send()
        container.viewContext.delete(object)
        save()

        Logger.coreData.debug("Deleted lift event (object)")
    }

    func liftEventsForSelectedFilter() -> [LiftEvent] {
        let request = LiftEvent.fetchRequest()
        request.includesPendingChanges = false
        let allLiftEvents = (try? container.viewContext.fetch(request)) ?? []
        return allLiftEvents
    }

    func hasLiftEvents() -> Bool {
        var hasLiftEvents = false
        let fetchRequest = NSFetchRequest<NSNumber>(entityName: "LiftEvent")
        fetchRequest.includesPendingChanges = false
        fetchRequest.resultType = .countResultType

        do {
            let countResult = try container.viewContext.fetch(fetchRequest)
            hasLiftEvents = countResult.first!.intValue > 0

            Logger.coreData.info("There are (countResult) lift events in the log")
        } catch let error {
            Logger.coreData.info("Error checking for lift events: (error)")

        }

        return hasLiftEvents
    }
}
extension LiftEvent {
    var eventID: String {
        uuid ?? ""
    }

    var eventCreationDate: Date {
        date ?? .now
    }

    var eventLift: Lift {
        if let lift = lift {
            return lift
        } else {
            if let context = self.managedObjectContext {
                let newLift = Lift(context: context)
                return newLift
            } else {
                fatalError("Managed Object Context is nil.")
            }
        }
    }

    var eventLiftName: String {
        get { lift?.liftName ?? "None" }
    }

    var eventLocalizedLiftName: String {
        return NSLocalizedString(eventLiftName, tableName: "liftLogModel", comment: "")
    }

    var eventFormulaName: String {
        formula?.formulaName ?? "None"
    }

    var eventReps: String {
        get {
            guard let reps = repetitions, reps.intValue != 0 else {
                return ""
            }
            return String(describing: reps)
        }
        set { repetitions = NSNumber(value: Double(newValue) ?? 0) }
    }

    var eventWeight: String {
        get {
            guard let weight = weightLifted, weight.intValue != 0 else {
                return ""
            }
            return String(describing: weight)
        }
        set { weightLifted = NSNumber(value: Double(newValue) ?? 0) }
    }

    var eventWeightUnit: WeightUnit {
        get {
            if let rawValue = weightUnit {
                return WeightUnit(rawValue: rawValue) ?? .kilograms
            }
            return .kilograms
        }
        set {
            weightUnit = newValue.rawValue
        }
    }
}

Log messages:


Context saved

There are [1] lift events in the log

>>>User swiped to delete

Events before deletion: [<LiftEvent: 0x600002129220> (entity: LiftEvent; id: 0x83602b006ad823c5 <x-coredata://DF552D0A-935D-4873-9114-CFB3C5CAAC8F/LiftEvent/p33>; data: {
    date = "2024-05-16 17:43:07 +0000";
    formula = "0x83602b006e3823f5 <x-coredata://DF552D0A-935D-4873-9114-CFB3C5CAAC8F/Formula/p6>";
    lift = "0x83602b006fb823e5 <x-coredata://DF552D0A-935D-4873-9114-CFB3C5CAAC8F/Lift/p10>";
    repetitions = 3;
    uuid = "9B7F9CFA-1E3B-45E8-99B7-4D74A3428FD9";
    weightLifted = 205;
    weightUnit = kg;
})]

Context saved

Deleted lift event <LiftEvent: 0x600002129220> (entity: LiftEvent; id: 0x83602b006ad823c5 <x-coredata://DF552D0A-935D-4873-9114-CFB3C5CAAC8F/LiftEvent/p33>; data: <fault>)

Events after deletion: [<LiftEvent: 0x600002129220> (entity: LiftEvent; id: 0x83602b006ad823c5 <x-coredata://DF552D0A-935D-4873-9114-CFB3C5CAAC8F/LiftEvent/p33>; data: <fault>)]

There are [1] lift events in the log

>>>User swiped to delete

Events before deletion: [<LiftEvent: 0x60000213ee90> (entity: LiftEvent; id: 0x83602b006ab823c5 <x-coredata://DF552D0A-935D-4873-9114-CFB3C5CAAC8F/LiftEvent/p34>; data: {
    date = "2024-05-16 17:43:14 +0000";
    formula = "0x83602b006e3823f5 <x-coredata://DF552D0A-935D-4873-9114-CFB3C5CAAC8F/Formula/p6>";
    lift = "0x83602b006fb823e5 <x-coredata://DF552D0A-935D-4873-9114-CFB3C5CAAC8F/Lift/p10>";
    repetitions = 0;
    uuid = "AFAB249F-0F14-4088-A46A-73A4060BCB4F";
    weightLifted = 0;
    weightUnit = kg;
})]

Context saved

Deleted lift event <LiftEvent: 0x60000213ee90> (entity: LiftEvent; id: 0x83602b006ab823c5 <x-coredata://DF552D0A-935D-4873-9114-CFB3C5CAAC8F/LiftEvent/p34>; data: <fault>)

Events after deletion: [<LiftEvent: 0x60000213ee90> (entity: LiftEvent; id: 0x83602b006ab823c5 <x-coredata://DF552D0A-935D-4873-9114-CFB3C5CAAC8F/LiftEvent/p34>; data: <fault>)]

There are [0] lift events in the log

LEAVE A COMMENT