Coredata - UI doesn't update after merging in SwiftUI - ios

First of all, sorry for my English :) I found a weird behavior in core data with SwiftUI. After adding a new persistent store to my container, UI doesn't erase erased objects when I generate entities conflict. Merge policy was NSMergeByPropertyObjectTrumpMergePolicy so existing object in the store might be overwritten by new object, but they still appears with 'nil' values. Let's look at my codes.
My core data model looks like:
Core data singleton:
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "CoredataTest")
// Adding new stores makes problem!!
let defaultURL = NSPersistentContainer.defaultDirectoryURL()
let newURL = defaultURL.appendingPathComponent("MyStore.sqlite")
container.persistentStoreDescriptions += [NSPersistentStoreDescription(url: newURL)]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.automaticallyMergesChangesFromParent = true
}
static func testConflict() {
let newPerson = Person(context: shared.container.viewContext)
newPerson.id = UUID()
newPerson.name = "Jack"
try! shared.container.viewContext.save()
let conflict = Person(context: shared.container.viewContext)
// Same id to test conflict
conflict.id = newPerson.id
conflict.name = "Another Jack"
try! shared.container.viewContext.save()
}
}
The new persistent store was never used. It's just for demonstrating core data's weird stuff.
UI:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Person.name, ascending: true)],
animation: .default)
private var people: FetchedResults<Person>
var body: some View {
NavigationView {
List {
ForEach(people) { person in
Text(person.name ?? "nil")
}
}
.toolbar {
ToolbarItem {
Button(action: PersistenceController.testConflict) {
Label("Make doppelganger", systemImage: "wand.and.stars")
}
}
}
}
}
}
When I tap the wand button to test conflict, that happens I just introduced you.
I tapped twice. There's nil objects which were "Jack" objects that doesn't exist anymore. They disappears when I close the app and launch it again that means they were erased in the store actually but UI still refers them in memory in some reasons.
It doesn't happen if I use only one persistent store. So, I guess using multiple persistent store is related but I have no ideas how to find the reason.
Of course, I can simply filter the nil objects to hide in the UI but I still want to know why this happens and there is any way to fix the fundamental problem.

Related

What are some reliable mechanism to prevent data duplication in CoreData CloudKit?

Every of our data row, contains an unique uuid column.
Previously, before adopting CloudKit, the uuid column has a unique constraint. This enables us to prevent data duplication.
Now, we start to integrate CloudKit, into our existing CoreData. Such unique constraint is removed. The following user flow, will cause data duplication.
Steps to cause data duplication when using CloudKit
Launch the app for the first time.
Since there is empty data, a pre-defined data with pre-defined uuid is generated.
The pre-defined data is sync to iCloud.
The app is uninstalled.
The app is re-installed.
Launch the app for the first time.
Since there is empty data, a pre-defined data with pre-defined uuid is generated.
Previous old pre-defined data from step 3, is sync to the device.
We are now having 2 pre-defined data with same uuid! :(
I was wondering, is there a way for us to prevent such duplication?
In step 8, we wish we have a way to execute such logic before written into CoreData
Check whether such uuid exists in CoreData. If not, write to CoreData.
If not, we will pick the one with latest update date, then overwrite
the existing data.
I once try to insert the above logic into https://developer.apple.com/documentation/coredata/nsmanagedobject/1506209-willsave . To prevent save, I am using self.managedObjectContext?.rollback(). But it just crash.
Do you have any idea, what are some reliable mechanism I can use, to prevent data duplication in CoreData CloudKit?
Additional info:
Before adopting CloudKit
We are using using the following CoreData stack
class CoreDataStack {
static let INSTANCE = CoreDataStack()
private init() {
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
precondition(Thread.isMainThread)
let container = NSPersistentContainer(name: "xxx", managedObjectModel: NSManagedObjectModel.wenote)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
// So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
// persistent store.
container.viewContext.automaticallyMergesChangesFromParent = true
// TODO: Not sure these are required...
//
//container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
//container.viewContext.undoManager = nil
//container.viewContext.shouldDeleteInaccessibleFaults = true
return container
}()
Our CoreData data schema has
Unique constraint.
Deny deletion rule for relationship.
Not having default value for non-null field.
After adopting CloudKit
class CoreDataStack {
static let INSTANCE = CoreDataStack()
private init() {
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
precondition(Thread.isMainThread)
let container = NSPersistentCloudKitContainer(name: "xxx", managedObjectModel: NSManagedObjectModel.wenote)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
// So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
// persistent store.
container.viewContext.automaticallyMergesChangesFromParent = true
// TODO: Not sure these are required...
//
//container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
//container.viewContext.undoManager = nil
//container.viewContext.shouldDeleteInaccessibleFaults = true
return container
}()
We change the CoreData data schema to
Not having unique constraint.
Nullify deletion rule for relationship.
Having default value for non-null field.
Based on a feedback of a Developer Technical Support engineer from https://developer.apple.com/forums/thread/699634?login=true , hen mentioned we can
Detecting Relevant Changes by Consuming Store Persistent History
Removing Duplicate Data
But, it isn't entirely clear on how it should be implemented, as the github link provided is broken.
There is no unique constraint feature once we have integrated with CloudKit.
The workaround on this limitation is
Once duplication is detected after insertion by CloudKit, we will
perform duplicated data deletion.
The challenging part of this workaround is, how can we be notified when there is insertion performed by CloudKit?
Here's step-by-step on how to be notified when there is insertion performed by CloudKit.
Turn on NSPersistentHistoryTrackingKey feature in CoreData.
Turn on NSPersistentStoreRemoteChangeNotificationPostOptionKey feature in CoreData.
Set viewContext.transactionAuthor = "app". This is an important step so that when we query on transaction history, we know which DB transaction is initiated by our app, and which DB transaction is initiated by CloudKit.
Whenever we are notified automatically via NSPersistentStoreRemoteChangeNotificationPostOptionKey feature, we will start to query on transaction history. The query will filter based on transaction author and last query token. Please refer to the code example for more detailed.
Once we have detected the transaction is insert, and it operates on our concerned entity, we will start to perform duplicated data deletion, based on concerned entity
Code example
import CoreData
class CoreDataStack: CoreDataStackable {
let appTransactionAuthorName = "app"
/**
The file URL for persisting the persistent history token.
*/
private lazy var tokenFile: URL = {
return UserDataDirectory.token.url.appendingPathComponent("token.data", isDirectory: false)
}()
/**
Track the last history token processed for a store, and write its value to file.
The historyQueue reads the token when executing operations, and updates it after processing is complete.
*/
private var lastHistoryToken: NSPersistentHistoryToken? = nil {
didSet {
guard let token = lastHistoryToken,
let data = try? NSKeyedArchiver.archivedData( withRootObject: token, requiringSecureCoding: true) else { return }
if !UserDataDirectory.token.url.createCompleteDirectoryHierarchyIfDoesNotExist() {
return
}
do {
try data.write(to: tokenFile)
} catch {
error_log(error)
}
}
}
/**
An operation queue for handling history processing tasks: watching changes, deduplicating tags, and triggering UI updates if needed.
*/
private lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
static let INSTANCE = CoreDataStack()
private init() {
// Load the last token from the token file.
if let tokenData = try? Data(contentsOf: tokenFile) {
do {
lastHistoryToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
error_log(error)
}
}
}
deinit {
deinitStoreRemoteChangeNotification()
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
precondition(Thread.isMainThread)
let container = NSPersistentCloudKitContainer(name: "xxx", managedObjectModel: NSManagedObjectModel.xxx)
// turn on persistent history tracking
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
// Provide transaction author name, so that we can know whether this DB transaction is performed by our app
// locally, or performed by CloudKit during background sync.
container.viewContext.transactionAuthor = appTransactionAuthorName
// So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
// persistent store.
container.viewContext.automaticallyMergesChangesFromParent = true
// TODO: Not sure these are required...
//
//container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
//container.viewContext.undoManager = nil
//container.viewContext.shouldDeleteInaccessibleFaults = true
// Observe Core Data remote change notifications.
initStoreRemoteChangeNotification(container)
return container
}()
private(set) lazy var backgroundContext: NSManagedObjectContext = {
precondition(Thread.isMainThread)
let backgroundContext = persistentContainer.newBackgroundContext()
// Provide transaction author name, so that we can know whether this DB transaction is performed by our app
// locally, or performed by CloudKit during background sync.
backgroundContext.transactionAuthor = appTransactionAuthorName
// Similar behavior as Android's Room OnConflictStrategy.REPLACE
// Old data will be overwritten by new data if index conflicts happen.
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
// TODO: Not sure these are required...
//backgroundContext.undoManager = nil
return backgroundContext
}()
private func initStoreRemoteChangeNotification(_ container: NSPersistentContainer) {
// Observe Core Data remote change notifications.
NotificationCenter.default.addObserver(
self,
selector: #selector(storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator
)
}
private func deinitStoreRemoteChangeNotification() {
NotificationCenter.default.removeObserver(self)
}
#objc func storeRemoteChange(_ notification: Notification) {
// Process persistent history to merge changes from other coordinators.
historyQueue.addOperation {
self.processPersistentHistory()
}
}
/**
Process persistent history, posting any relevant transactions to the current view.
*/
private func processPersistentHistory() {
backgroundContext.performAndWait {
// Fetch history received from outside the app since the last token
let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest!
historyFetchRequest.predicate = NSPredicate(format: "author != %#", appTransactionAuthorName)
let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
request.fetchRequest = historyFetchRequest
let result = (try? backgroundContext.execute(request)) as? NSPersistentHistoryResult
guard let transactions = result?.result as? [NSPersistentHistoryTransaction] else { return }
if transactions.isEmpty {
return
}
for transaction in transactions {
if let changes = transaction.changes {
for change in changes {
let entity = change.changedObjectID.entity.name
let changeType = change.changeType
let objectID = change.changedObjectID
if entity == "NSTabInfo" && changeType == .insert {
deduplicateNSTabInfo(objectID)
}
}
}
}
// Update the history token using the last transaction.
lastHistoryToken = transactions.last!.token
}
}
private func deduplicateNSTabInfo(_ objectID: NSManagedObjectID) {
do {
guard let nsTabInfo = try backgroundContext.existingObject(with: objectID) as? NSTabInfo else { return }
let uuid = nsTabInfo.uuid
guard let nsTabInfos = NSTabInfoRepository.INSTANCE.getNSTabInfosInBackground(uuid) else { return }
if nsTabInfos.isEmpty {
return
}
var bestNSTabInfo: NSTabInfo? = nil
for nsTabInfo in nsTabInfos {
if let _bestNSTabInfo = bestNSTabInfo {
if nsTabInfo.syncedTimestamp > _bestNSTabInfo.syncedTimestamp {
bestNSTabInfo = nsTabInfo
}
} else {
bestNSTabInfo = nsTabInfo
}
}
for nsTabInfo in nsTabInfos {
if nsTabInfo === bestNSTabInfo {
continue
}
// Remove old duplicated data!
backgroundContext.delete(nsTabInfo)
}
RepositoryUtils.saveContextIfPossible(backgroundContext)
} catch {
error_log(error)
}
}
}
Reference
https://developer.apple.com/documentation/coredata/synchronizing_a_local_store_to_the_cloud - In the sample code, the file CoreDataStack.swift illustrate a similar example, on how to remove duplicated data after cloud sync.
https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes - Information on transaction histories.
What's the best approach to prefill Core Data store when using NSPersistentCloudKitContainer? - A similar question

Core Data: NSFetchRequest returning no error, but zero values

I am refactoring my existing core data manager to use an NSPersistentContainer. I have what seems to be some relatively straightforward code to fetch my entities:
class CoreDataManager {
static let shared: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DateAid")
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { storeDescription, error in
if let error = error {
print(error.localizedDescription)
}
}
return container
}()
static func fetch<T: NSManagedObject>() throws -> [T] {
let context = shared.viewContext
let request = NSFetchRequest<T>(entityName: String(describing: T.self))
do {
return try context.fetch(request) as [T]
} catch {
throw error
}
}
}
However, in another class where I call this code, it retrieves nothing. I am calling it like so:
do {
let events: [Event] = try CoreDataManager.fetch()
} catch {
throw error
}
It's not erroring out, but it's not retrieving anything. Hence, I can't get any idea what's going wrong. Things to note:
My xcdatamodel is correctly named "DateAid"
Breakpoints are hitting in the container's loadPersistentStores and I can print to the console a proper storeDescription that shows the path to the sqlite file
String(describing: T.self) works fine, as it returns "Event" (I've tried hard-coding the string "Event" as well to no avail)
the persistentContainer's properties all seem to be set.
I know the entities are all still there because when I go back to my existing implementation (with all the manual core data setup in the app delegate), it properly fetches the entities.
What could the issue be? Is there a chance it could be some kind of sync/async race condition issue happening outside of my example? Am I missing something obvious? Any help or direction to either solve or figure out how to debug the problem is greatly appreciated.
loadPersistentStores is never called in your implementation.
instead of doing this (which you never call)
private var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DateAid")
container.loadPersistentStores { storeDescription, error in
if let error = error {
print(error.localizedDescription)
}
}
return container
}()
create singleton instance of your NSPersistentContainer like this:
static let shared: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DateAid")
container.loadPersistentStores { storeDescription, error in
if let error = error {
print(error.localizedDescription)
}
}
return container
}()
Note:
static let shared = CoreDataManager()
Should be removed, because it is not doing anything at the moment.
Edit 1:
I just saw that you are loading persistent store each time you make fetch request, you should load the store only once because it is adding unnecessary work each time you perform a fetch request.
Edit 2:
You can make the CoreDataManager subclass of NSPersistentContainer and you can access the viewContext right within the class without shared.viewContext
Quote from Apple
NSPersistentContainer is intended to be subclassed. Your subclass is a
convenient place to put Core Data–related code like functions that
return subsets of data and calls to persist data to disk.
Edit 3 - How to:
Subclassing
class CoreDataManager: NSPersistentContainer {
private override init(name: String, managedObjectModel model: NSManagedObjectModel) {
super.init(name: name, managedObjectModel: model)
}
public static let shared: CoreDataManager = {
let container = CoreDataManager(name: "DateAid")
container.loadPersistentStores { storeDescription, error in
if let error = error {
print(error.localizedDescription)
}
}
try? container.viewContext.setQueryGenerationFrom(.current)
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
public func fetch<T: NSManagedObject>() throws -> [T] {
let request = NSFetchRequest<T>(entityName: String(describing: T.self))
do {
return try self.viewContext.fetch(request) as [T]
} catch {
throw error
}
}
}
Fetch request
do {
let events: [Event] = try CoreDataManager.shared.fetch()
} catch {
throw error
}

CoreData context saveing bad performance on a big dataset

I have got a big dataset (about 3000 items) of MoodEntry objects in CoreData. I have performance issues when adding a new item: animation takes about 2 seconds. Here is the code:
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: MoodEntry.getAllMoodItems()) var moodEntries: FetchedResults<MoodEntry>
.
Button(action: {
let moodEntry = MoodEntry(context: context)
moodEntry.value = 1
moodEntry.date = Date()
moodEntry.id = UUID()
do {
try context.save()
} catch {
print(error)
}
let impactLight = UIImpactFeedbackGenerator(style: .light)
impactLight.impactOccurred()
}) { Image("Button1").renderingMode(.original) }
Does the problem occur because I save all context every time new item is added? Is there a simple solution to this?
Did you already try to execute everything in the background thread?
Button(action: {
DispatchQueue.global(qos: .background).async {
let moodEntry = MoodEntry(context: context)
moodEntry.value = 1
moodEntry.date = Date()
moodEntry.id = UUID()
do {
try context.save()
} catch {
print(error)
}
}
let impactLight = UIImpactFeedbackGenerator(style: .light)
impactLight.impactOccurred()
}) { Image("Button1").renderingMode(.original) }
What animation is slow? I’m guessing you have a List that shows the results of that fetch request, and that’s what is slow to update. If so: it’s slow because SwiftUI is diff’ing every row. One workaround is to set .id(UUID()) on the List so the whole thing is regenerated without diffing the rows; here’s an explanation: https://www.hackingwithswift.com/articles/210/how-to-fix-slow-list-updates-in-swiftui

iOS swift core data persistence problem: Lost and thrashing

I may have done a really stupid thing.
In writing an upgrade for my (first) shipped app, I changed the schema of my Core Data model, adding another entity (Issuer) and creating a to-many relationship with the existing entity (Coin). I thought the lightweight migration had successfully taken care of the change:
Before:
After:
Here's the Core Data stack, created automatically when I created the project:
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Coin_Portfolio")
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
In testing on my iPhone, I found that I could create Coins, like this:
func createNewCoin(){
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let context = appDelegate.persistentContainer.viewContext
newCoin = NSEntityDescription.insertNewObject(forEntityName: "Coin", into: context) as! Coin
if currentOwner.name == nil {
print("In NewCoin, createNewCoin, currentOwner is nil")
}else{
print("In NewCoin, createNewCoin, currentOwner is named \(String(describing: currentOwner.name))")
}
// Problem with contexts could be here! Compare DB for this app to test bed
newCoin.denomination = nil
newCoin.grade = nil
newCoin.itemIdentifier = ""
newCoin.mintMark = nil
currentOwner.addToCoins(newCoin)
saveIt()
}
func saveIt(){
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
appDelegate.saveContext()
}
And retrieve it like this:
let context = appDelegate.persistentContainer.viewContext
let ownerFetchRequest :
NSFetchRequest<Issuer> = Issuer.fetchRequest()
do {
try ownerFRC?.performFetch()
owners = try context.fetch(ownerFetchRequest)
} catch {
fatalError("Failed to initialize FetchedResultsController: \(error)")
}
for owner in owners{
if owner.name == currentOwner.name{
currentOwnerCoinsByDenom = ((owner.coins?.allObjects as! [Coin]).sorted(by: { $0.denomination! < $1.denomination! }))
currentOwnerCoinsByYear = ((currentOwnerCoinsByDenom as! [Coin]).sorted(by: { $0.denomination! < $1.denomination! }))
print ("currentOwnerCoinsByDenom.count == \(currentOwnerCoinsByDenom)")
print(currentOwnerCoinsByDenom.count)
print ("currentOwnerCoinsByYear.count == \(currentOwnerCoinsByYear)")
print(currentOwnerCoinsByYear.count)
}
}
}
Fine, as long as I don't quit the app and relaunch it.
Upon relaunch, my fetch attempts return no Coins, even though I know that there had been some 80+ records stored in Core Data before the schema change.
I then started what I thought was a rational approach to solving the problem. See iOS data saved in Core Data doesn't survive launch. The answers received, unfortunately, didn't solve the problem.
Accordingly, I panicked, knowing my users wouldn't take kindly to losing their own records.
Here's where I may have run off the rails: I created a duplicate project in order to try to resolve the issue without screwing up my original. But I continue to experience the same problem.
I have examined the dataBase(s) using DB Browser for SQLite, but am very confused as to which DB I'm downloading -- is it the original, or the one created by the duplicate project? They both have the same name, but one of them has the original records, the other only one record.
Any ideas how I can save myself? I'm flummoxed and desperate! Will gladly supply any needed info or code.
All help deeply appreciated!

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.
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
add new state
#State var documents: [ScanDocument] = []
#State private var refreshID = UUID() // can be actually anything, but unique
make List identified by it
List(documents, id: \.id) { item in
ZStack {
DocumentCell(document: item)
}
}.id(refreshID) // << here
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
...
}
Change
var document: ScanDocument
to
#ObservedObject var document: ScanDocument
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])
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)
}
}

Resources