rv7284
rv7284

Reputation: 1102

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.

Upvotes: 10

Views: 7355

Answers (5)

alionthego
alionthego

Reputation: 9773

For my case when you come back to the view with the list I call

viewContext.refreshAllObjects()

and it seems to be working so far. The advantage over setting a refreshID is your whole view is not redrawn so your scroll position on the list is maintained.

I call this in an .onChange for the bool that presented the other view:

.onChange(of: showOtherView, { oldValue, newValue in
     if newValue == false {
          viewContext.refreshAllObjects()
     }
}

You can also just refresh a single object by calling:

viewContext.refresh(objectYouChanged, mergeChanges: true)

Upvotes: 1

Nik
Nik

Reputation: 9462

Change

var document: ScanDocument

to

@ObservedObject var document: ScanDocument

Upvotes: 5

Asperi
Asperi

Reputation: 258365

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
 
   ...
}

Upvotes: 21

andrewbuilder
andrewbuilder

Reputation: 3799

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)
    }
}

Upvotes: 0

Victor Kushnerov
Victor Kushnerov

Reputation: 3974

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])

Upvotes: 1

Related Questions