SwiftUI List not updating when core data property is updated in other view
Asked Answered
B

5

10
@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.

Buckler answered 26/7, 2020 at 13:50 Comment(0)
S
20

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
 
   ...
}
Sihon answered 26/7, 2020 at 14:1 Comment(3)
For some reason, does not work for me.Kikelia
It works! but honestly... there is no other way? Why is this happening? I mean, I have an array objects, this objects have an arrays of X, in a detail screen I update an X object, so I expect the first screen listen to this changes... but just don't happen.Aleksandropol
This solution worked perfectly for my issue! My issue was similar in that I was trying to update a Core Data property from 2 separate views. Specifically an on-screen element XY position. I wonder if implementing a MVVM setup would help avoid the issue. Thanks!Schleicher
A
5

Change

var document: ScanDocument

to

@ObservedObject var document: ScanDocument
Acker answered 20/4, 2022 at 13:27 Comment(0)
A
1

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])
Amandie answered 29/4, 2021 at 12:22 Comment(0)
C
1

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)
Copperas answered 8/12, 2023 at 12:29 Comment(0)
R
0

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)
    }
}
Reborn answered 23/11, 2021 at 2:2 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.