SwiftUI: List does not update automatically after deleting all Core Data Entity entries
Asked Answered
F

2

16

I know SwiftUI uses state-driven rendering. So I was assuming, when I delete Core Data Entity entries, that my List with Core Data elements gets refreshed immediately. I use this code, which gets my Entity cleaned succesfully:

func deleteAll()
{
    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ToDoItem.fetchRequest()
    let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)

    let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer

    do {
        try persistentContainer.viewContext.execute(deleteRequest)
    } catch let error as NSError {
        print(error)
    }
}

To get the List in my View visually empty I have to leave the View afterwards (for example with " self.presentationMode.wrappedValue.dismiss()") and open it again. As if the values are still stored somewhere in the memory or something. This is of course not user-friendly and I am sure I just oversee something that refreshes the List immediately. Maybe someone can help.

Florettaflorette answered 14/2, 2020 at 16:34 Comment(0)
L
24

The reason is that execute (as described in details below - pay attention on first sentence) does not affect managed objects context, so all fetched objects remains in context and UI represents what is really presented by context.

So in general, after this bulk operation you need to inform back to that code (not provided here) force sync and refetch everything.

API interface declaration

// Method to pass a request to the store without affecting the contents of the managed object context.
// Will return an NSPersistentStoreResult which may contain additional information about the result of the action
// (ie a batch update result may contain the object IDs of the objects that were modified during the update).
// A request may succeed in some stores and fail in others. In this case, the error will contain information
// about each individual store failure.
// Will always reject NSSaveChangesRequests.
@available(iOS 8.0, *)
open func execute(_ request: NSPersistentStoreRequest) throws -> NSPersistentStoreResult

For example it might be the following approach (scratchy)

// somewhere in View declaration
@State private var refreshingID = UUID()

...
// somewhere in presenting fetch results
ForEach(fetchedResults) { item in
    ...
}.id(refreshingID) // < unique id of fetched results

...

// somewhere in bulk delete 
try context.save() // < better to save everything pending
try context.execute(deleteRequest)
context.reset() // < reset context
self.refreshingID = UUID() // < force refresh
Languishing answered 14/2, 2020 at 17:19 Comment(2)
I had the same problem with updates after adding records in a separate view. Since I only use the context in my List view to perform deletes, I reset the context and refresh the ID's in .onAppear. I don't fully understand yet why it's necessary or even why it works but it does, so thanks!Grief
this is not the best solution as .id will regenerate the whole list instead of just update the changesCatheycathi
R
26

No need to force a refresh, this is IMO not a clean solution.

As you correctly mentioned in your question, there are still elements in memory. The solution is to update your in-memory objects after the execution with mergeChanges.

This blog post explains the solution in detail under "Updating in-memory objects".

There, the author provides an extension to NSBatchDeleteRequest as follows

extension NSManagedObjectContext {
    
    /// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
    ///
    /// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
    /// - Throws: An error if anything went wrong executing the batch deletion.
    public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
        batchDeleteRequest.resultType = .resultTypeObjectIDs
        let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
        let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
        NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
    }
}

Here is an update to your code on how to call it:

func deleteAll() {
    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ToDoItem.fetchRequest()
    let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)

    let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer

    do {
        try persistentContainer.viewContext.executeAndMergeChanges(deleteRequest)
    } catch let error as NSError {
        print(error)
    }
}

Some more info also here under this link: Core Data NSBatchDeleteRequest appears to leave objects in context.

Righthand answered 17/2, 2020 at 15:53 Comment(0)
L
24

The reason is that execute (as described in details below - pay attention on first sentence) does not affect managed objects context, so all fetched objects remains in context and UI represents what is really presented by context.

So in general, after this bulk operation you need to inform back to that code (not provided here) force sync and refetch everything.

API interface declaration

// Method to pass a request to the store without affecting the contents of the managed object context.
// Will return an NSPersistentStoreResult which may contain additional information about the result of the action
// (ie a batch update result may contain the object IDs of the objects that were modified during the update).
// A request may succeed in some stores and fail in others. In this case, the error will contain information
// about each individual store failure.
// Will always reject NSSaveChangesRequests.
@available(iOS 8.0, *)
open func execute(_ request: NSPersistentStoreRequest) throws -> NSPersistentStoreResult

For example it might be the following approach (scratchy)

// somewhere in View declaration
@State private var refreshingID = UUID()

...
// somewhere in presenting fetch results
ForEach(fetchedResults) { item in
    ...
}.id(refreshingID) // < unique id of fetched results

...

// somewhere in bulk delete 
try context.save() // < better to save everything pending
try context.execute(deleteRequest)
context.reset() // < reset context
self.refreshingID = UUID() // < force refresh
Languishing answered 14/2, 2020 at 17:19 Comment(2)
I had the same problem with updates after adding records in a separate view. Since I only use the context in my List view to perform deletes, I reset the context and refresh the ID's in .onAppear. I don't fully understand yet why it's necessary or even why it works but it does, so thanks!Grief
this is not the best solution as .id will regenerate the whole list instead of just update the changesCatheycathi

© 2022 - 2024 — McMap. All rights reserved.