I had similar problems with an expenses management app, where I have a hierarchy of Groupings (master) with Transactions (detail) against the lowest Grouping in the hierarchy. The app uses CoreData with CloudKit sync and is multi-platform., both of which add complexity to the design approach.
My solution is this:
In the CoreData model there are two entities, Grouping and Transaction: with a one-to-many relationship from Grouping to Transaction and one-to-one reciprocal from Transaction to Grouping. A Fetch of a Grouping (master), or set of Groupings, therefore has the transactions (detail) as an NSManaged NSSet on each Grouping (master) record. In the Xcode editor of the CoreData model I use Manual Codegen to put the NSManagedObject entity, properties and accessors in my project. This allows for easier coding of extensions to create computed properties on the CoreData entities, e.g. summing transactions for a Grouping.
I have a DataStore ObservableObject class singleton that handles all CoreData / CloudKit setup and data processing, e.g. all Fetches, insertions, deletions etc. For example, there is a DataStore method to fetch all Groupings (master records) “getAllGroupings” which returns an array of CoreData Grouping objects, each of which has its own set of Transaction records (detail). So, in a SwiftUI View, I have a @ObservedObject var dataStore = DataStore.shared, which means that I can therefore refer to the Groupings array in a List by using List(dataStore.getAllGroupings()) ….. To get the list of Transactions in another (detail) view, I pass the required Grouping (master record) to the detail view as a var, then use List(grouping.transactionsArray) - where “transactionsArray” is a computed property on my Grouping CoreData extension that turns an NSSet into a Swift Array. **** This particular solution doesn't need datastore to be an ObservedObject, but I use that for other reasons in the app.
To delete a transaction in detail view, I call a method on dataStore e.g. deleteTransaction(transaction: Transaction) which does a context.delete(transaction) and then a context save. This deletes the transaction and its entry in the Grouping’s transaction NSSet (according to the delete rules I set in CoreData).
HOWEVER: this delete method (or add a transaction) does not update the view, because the changes are made to the CoreData context, which is not directly observable by SwiftUI. So, in DataStore I have a Combine property to send a notification public let transactionDeleted = PassthroughSubject<(String), Never>() then in the “deleteTransaction” method, I use transactionDeleted.send(“txn deleted”) - you can set up the PassthroughSubject to send whatever you want.
Either in your Master View or DetailView (depends on your processing - need to experiment), have
.onReceive((dataStore.transactionDeleted), perform: { transaction in
self.statusTime = Date()
})
This updates a @State var statusTime which, unfortunately, needs to be used somewhere in the view: this triggers a View refresh and, voila, the deletion no longer appears in the View.
NOTE: I use public for my methods and properties in DataStore because this is a Multiplatform app and DataStore is therefore in a framework. This is also why I use Manual CodeGen in Xcode's CoreData models, i.e. to use in a framework.
I also use the PassthroughSubject in non-CoreData apps where there’s complex data processing, which if ObservedObjects are used can cause unnecessary view refreshes. In these cases, once all processing has completed I then issue the PassthroughSubject send to update the view.
I hope this helps. Regards, Michaela