'SwiftUI List not updating when core data property is updated in other view

@State var documents: [ScanDocument] = []

func loadDocuments() {
    guard let appDelegate =
        UIApplication.shared.delegate as? AppDelegate else {
            return
    }
    
    let managedContext =
        appDelegate.persistentContainer.viewContext
    
    let fetchRequest =
        NSFetchRequest<NSManagedObject>(entityName: "ScanDocument")
    
    do {
        documents = try managedContext.fetch(fetchRequest) as! [ScanDocument]
        print(documents.compactMap({$0.name}))
    } catch let error as NSError {
        print("Could not fetch. \(error), \(error.userInfo)")
    }
}

In the first view:

.onAppear(){
     self.loadDocuments()
 }

Now I'm pushing to detail view one single object:

NavigationLink(destination: RenameDocumentView(document: documents[selectedDocumentIndex!]), isActive: $pushActive) {
                    Text("")
                }.hidden()

In RenameDocumentView:

var document: ScanDocument

Also, one function to update the document name:

func renameDocument() {
    guard !fileName.isEmpty else {return}
    document.name = fileName
    try? self.moc.save()
    print(fileName)
    self.presentationMode.wrappedValue.dismiss()
}

All this code works. This print statement always prints updated value:

print(documents.compactMap({$0.name}))

Here's the list code in main View:

List(documents, id: \.id) { item in
     ZStack {
          DocumentCell(document: item)
     }
}

But where user comes back to previous screen. The list shows old data. If I restart the app it shows new data.

Any help of nudge in a new direction would help.

There is a similar question here: SwiftUI List View not updating after Core Data entity updated in another View, but it's without answers.



Solution 1:[1]

NSManagedObject is a reference type so when you change its properties your documents is not changed, so state does not refresh view.

Here is a possible approach to force-refresh List when you comes back

  1. add new state
@State var documents: [ScanDocument] = []
@State private var refreshID = UUID()   // can be actually anything, but unique
  1. make List identified by it
List(documents, id: \.id) { item in
     ZStack {
          DocumentCell(document: item)
     }
}.id(refreshID)     // << here
  1. change refreshID when come back so forcing List rebuild
NavigationLink(destination: RenameDocumentView(document: documents[selectedDocumentIndex!])
                               .onDisappear(perform: {self.refreshID = UUID()}), 
                isActive: $pushActive) {
                    Text("")
                }.hidden()

Alternate: Possible alternate is to make DocumentCell observe document, but code is not provided so it is not clear what's inside. Anyway you can try

struct DocumentCell: View {
   @ObservedObject document: ScanDocument
 
   ...
}

Solution 2:[2]

Core Data batch updates do not update the in-memory objects. You have to manually refresh afterwards.

Batch operations bypass the normal Core Data operations and operate directly on the underlying SQLite database (or whatever is backing your persistent store). They do this for benefits of speed but it means they also don't trigger all the stuff you get using normal fetch requests.

You need to do something like shown in Apple's Core Data Batch Programming Guide: Implementing Batch Updates - Updating Your Application After Execution

Original answer similar case similar case

let request = NSBatchUpdateRequest(entity: ScanDocument.entity())
request.resultType = .updatedObjectIDsResultType

let result = try viewContext.execute(request) as? NSBatchUpdateResult
let objectIDArray = result?.result as? [NSManagedObjectID]
let changes = [NSUpdatedObjectsKey: objectIDArray]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [managedContext])

Solution 3:[3]

An alternative consideration when attempting to provide a solution to this question is relating to type definition and your force down casting of your fetch request results to an array of ScanDocument object (i.e. [ScanDocument]).

Your line of code...

    documents = try managedContext.fetch(fetchRequest) as! [ScanDocument]

...is trying to force downcast your var documents to this type - an array of objects.

In fact an NSFetchRequest natively returns an NSFetchRequestResult, but you have already defined what type you are expecting from the var documents.

In similar examples where in my code I define an array of objects, I leave out the force downcast and the try will then attempt to return the NSFetchRequestResult as the already defined array of ScanDocument object.

So this should work...

    documents = try managedContext.fetch(fetchRequest)

Also I note you are using SwiftUI List...

Comment No.1

So you could try this...

List(documents, id: \.id) { item in
    ZStack {
        DocumentCell(document: item)
    }
    .onChange(of: item) { _ in
        loadDocuments()
    }
}

(Note: Untested)

But more to the point...

Comment No.2

Is there a reason you are not using the @FetchRequest or @SectionedFetchRequest view builders? Either of these will greatly simplify your code and make life a lot more fun.

For example...

@FetchRequest(entity: ScanDocument.entity(),
              sortDescriptors: [
                NSSortDescriptor(keyPath: \.your1stAttributeAsKeyPath, ascending: true),
                NSSortDescriptor(keyPath: \.your2ndAttributeAsKeyPath, ascending: true)
              ] // these are optional and can be replaced with []
) var documents: FetchedResults<ScanDocument>

List(documents, id: \.id) { item in
    ZStack {
        DocumentCell(document: item)
    }
}

and because all Core Data entities in SwiftUI are by default ObservedObjects and also conform to the Identifiable protocol, you could also leave out the id parameter in your List.

For example...

List(documents) { item in
    ZStack {
        DocumentCell(document: item)
    }
}

Solution 4:[4]

Change:

var document: ScanDocument

To:

@ObservedObject var document: ScanDocument

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Asperi
Solution 2
Solution 3 andrewbuilder
Solution 4 Skatox