SwiftUI update of deleted CoreData entities from different Tab

Hello,
I am quite new to iOS programming and I am currently implementing my first iOS app with SwiftUI and CoreData. I have a question and I hope you can help me.

My app has multiple tabs and my Core Data persistence objects can be fetched, manipulated and deleted from different Views and Tabs.

I have a Detail-View that is used for multiple pages accessible from Tab 1 and Tab 2.

So, it may be the case that a user opens the Detail-View on Tab 1, then switches to the same Detail-View on Tab 2.
If now the user deletes the persistence object used for the Detail-View on one of the Tabs, I want that the Detail-View gets hidden and want to transition back to the previous List-View.
However, If I switch to my other Tab, my Detail-View now still shows my Detail-View, but without any data (I checked if my attribute is nil).

I think it may be appropriate if the Detail-View will also disappear and switch to the parent List-View for the other Tab, if the persistence object does not exists anymore.
I found that I can check a persistence objects isFault attribute, which is true, after I deleted and saved my changes via the NSManagedObjectContext.
Code Block
var body: some View {
List {
if self.myPersistenceObject.isFault {
// persistence Object was deleted
Text(LocalizedStringKey("Persistence enity has fault"))
} else {
// persistence Object is alive
RealDetailView()
...

This works in a way, that I see a Text, if my persistence object is not available anymore and otherwise shows the real Detail-View.
But thats not quite that what I want…

I tried to check if if my persistence object isFault also .onAppear and set my Bool @Binding variable to false, if my persistence object is not available anymore.
Code Block
struct DetailView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@ObservedObject var myPersistenceObject: MyPersistenceObject
@Binding var presentView: Bool
...
.onAppear {
if myPersistenceObject.isFault {
self.presentView = false
}
}
struct ListViewTab1: View {
@ObservedObject var myPersistenceObject: MyPersistenceObject
@State var presentDetailView: Bool = false
var body: some View {
NavigationLink(
destination:
DetailView(myPersistenceObject: myPersistenceObject,
presentView: $presentDetailView),
isActive: $presentDetailView,
label: {
Text("Label")
}
)
}
}

The presentView Binding is passed from my List-View into my Detail-View.
However, dismissing my view with this Bool-Binding only works for one of my Tabs and not for the other one. That is, if I delete the object from the Detail-View in Tab 1 the Detail-View gets dismissed on deletion, but if I switch to the other Tab, the Detail-View has not been dismissed.

Has anybody also struggled with issues where SwiftUI views were not updated correctly after a CoreData persistence object was deleted?

My project has grown quite big already, so I have not yet a more complete example ready. However, maybe anybody also had that issue and solved it differently.
If not, I will work on a small reproducible example in the next weeks.

Anyway, thank you for reading my question and for any support.

Best,
Bernhard
How are you retrieving the managed object in the first place? Presumably you have an @FetchRequest or NSFetchedResultsController somewhere that is injecting the views? If not and you are performing a simple NSFetchRequest on a context you'll have to manage this yourself, but before diving into that could you show where the MyPersistenceObject is originating from?
I use @FetchRequest for both of my Tabs, but a bit differently.

In the Tab 1 my View has @FetchRequest that directly fetches instances of MyPersistenceObject:
Code Block swift
struct Tab1: View {
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(entity: MyPersistenceObject.entity(),
                  sortDescriptors: [])
    var myPersistenceObjects: FetchedResults<MyPersistenceObject>
...
ForEach(myPersistenceObjects, id: \.self) { myPersistenceObject in
ListViewTab1(myPersistenceObject: myPersistenceObject)
}

Instances of myPersistenceObject are then passed to my ListViewTab1 View (see above).

But, for the Tab 2, I also use @FetchRequest, but I am not directly fetching MyPersistenceObject but Parents of parents of my MyPersistenceObject.

Code Block swift
struct Tab2: View {
    @FetchRequest(entity: MyPersistenceObjectParentParent.entity(),
                  sortDescriptors: [])
    private var parentParents: FetchedResults<MyPersistenceObjectParentParent>
}


I then have NavigationLinks to a Tab2Child View, which receive one MyPersistenceObjectParentParent instance. This child then inits another FetchRequest in the init with some attribute
Code Block
struct Tab2Child: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @ObservedObject var parentParent: MyPersistenceObjectParentParent
    @FetchRequest
    var myPersistenceObjectParents: FetchedResults<MyPersistenceObjectParent>
    init(parentParent: MyPersistenceObjectParentParent) {
        self.parentParent = parentParent
        self._myPersistenceObjectParents = FetchRequest(entity: MyPersistenceObjectParent.entity(),
                                      sortDescriptors: [],
                                      predicate: NSPredicate(format: "parentParent = %@", argumentArray: [parentParent]))
    }

The view Tab2Child then has two further child views, which get passed one instance of MyPersistenceObjectParent.
The leaf child Detail-View(see above) then gets passed into the the persistent @ObservedObject MyPersistenceObject into the view, because the parent of the child-View uses a ForEach on one instance of the MyPersistenceObjectParent.
Code Block
struct Detail-View2-Parent: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @ObservedObject
    var myPersistenceObjectParent: MyPersistenceObjectParent
var body: some View {
        List {
                ForEach(myPersistenceObjectParent.myPersistenceObjects, id: \.self) { myPersistenceObject in
                    DetailView(myPersistenceObject: myPersistenceObject)
                }

Sorry, that its not really clearly structured. :(
In your final example, you are passing myPersistenceObjectParent.myPersistenceObjects into the ForEach view. Because NSManagedObjects are not notified of changes in their to-many relationships, when you call context.delete(myPersistenceObject) it does not trigger an update to the parent on line 5 of the final example and does not cause the view to be recomputed. I believe if you instead called the generated parent.removeFromMyPersistenceObjects(myPersistentObject) before calling context.delete it would cause an update to be triggered

The simple solution would be to also init a FetchRequest in this view to fetch the myPersistenceObjects rather than directly accessing the relationship property on myPersistenceObjectParent. Alternately you will need to create a new ObservableObject that takes in your parent, and observes changes to it's context to identify when its children are deleted/modified.

EDIT: I implied this, but to be clear @FetchRequest monitors changes to its query and triggers a view update for you, thereby removing the need for a separate model conforming to ObservableObject
hi,

your question made me realize i had something like this situation occurring as a very remote possibility in one of my projects. my solution (which works, as far as i have tested) is:
  • when a detail view is initialized, record the id of the object you display (you do have an id: UUID? attribute on the object, yes?).

  • with every .onAppear() of the DetailView, use a Core Data fetch to see whether you still have a CD object with that id.

  • if no such object exists, then just dismiss the detail view right away.

hope that helps,
DMG
Well, @jjatie I can see that my DetailView for a persistence entity in Tab2 gets automatically updated, if I change this persistence entity in the DetailView inside Tab1 on save and vice versa. Apparently, it's because @FetchRequest does automatically pull updates if possible.
But I am wondering, how I should handle the situation, where I have open the same DetailView on Tab 1 and Tab 2. User deletes Core Data entity on Tab 1 and switches back to Tab 2.
@DelawareMathGuy pointed out to save the persistent objectID for the current persistent entity in the DetailView and always check .onAppear, if the persistent object still exists with context.object(with: objectId) (where context is an instance of NSManagedObjectContext).
But what about persistentObject.isFault? Would you recommend to use it to check whether the current persistent entity in use for the DetailView still exists? Do you see it as an alternative to query the object by objectID or is this not a best-practice?

Different situation but also related to updating the View on persistent Object change:
But some entities that have relationships to other persistent entities are not automatically updated, if the related entity changed.
I use as workaround to always update my @FetchRequest results, where necessary this workaround in my view:
Code Block swift
struct DetailView: View {
    @State private var viewAppeared: Bool = false
var body: some View {
Group {
RealDetailView()
if viewAppeared {
                EmptyView()
}.onAppear {
            viewAppeared.toggle()
        }
}

This workaround causes my View to update if some persistent entities, that have a relationship to my current persistent object, have changes. I don't know if anybody has a better advice for that… But let me know. :-)



hi Bernhard,

you could take a look at my ShoppingList14 project over on GitHub, where i (sort of) solve the problem of having two edit views open on the same CD object in two different tabs and "delete" the object in one of those views. i make sure that the second one goes away before it can be seen when returning to the other tab.

you will find my comments about this situation in the file AddOrModifyItemView.swift. i can't say i know enough about using isFault, since it will be true when the object is deleted, but isDeleted will be false.

on the other item you mentioned, about changing a property of a CD object Y that's associated via some relationship with a CD object X, no change has been made to the attributes of X (only to the underlying NSSet or NSOrderedSet for the relationship of X). so X never sees a change to one of the properties in Y.

you can work around this; when a change is made to Y, find any and all X to which it is related and call X.objectWillChange.send(). you will find that i do this in ShoppingList14, and i have included some lengthy comments about this strategy, mostly for the case where X has computed properties that are based on the associated Ys (see Item+Extensions.swift and Location+Extensions.swift).

hope that helps,
DMG
@bernhard-st Whether or not the managed object is faulted is not an indication of it has been deleted or not, it is only an indication of whether the object is currently loaded into memory, so it is not best practice to rely on that for this purpose. Additionally, constantly fetching on the .onAppear is very inefficient.

There is a simple way to observe changes to the managedObjectContext through a Notification.
Code Block
struct NotifyOfDeletionView: View {
@Environment(\.managedObjectContext) var viewContext
@Environment(\.presentationMode) var presentationMode
@ObservedObject var managedObject: NSManagedObject
var body: some View {
Text(managedObject.objectID.description)
.onReceive(
NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: viewContext),
perform: dismissIfObjectIsDeleted(_:)
)
}
private func dismissIfObjectIsDeleted(_ notification: Notification) {
let user
if let userInfo = notification.userInfo,
let deletedObjectIDs = userInfo[NSManagedObjectContext.NotificationKey.deletedObjectIDs] as? Set<NSManagedObjectID>,
deletedObjectIDs.contains(managedObject.objectID) /* The object was deleted */
{
/* This will dismiss a modal (sheet/fullScreenCover) or pop the top view of a NavagationView stack */
presentationMode.wrappedValue.dismiss()
}
}
}

Note that you don't need to pass in an presentView binding. The environment has a PresentationMode as shown above.

The @FetchRequest approach I described previously is still a perfectly valid approach (and it is implemented under the hood using these Notifications or something similar), but there are still use cases for using Notifications like the above. Whatever tool feels right for your use case ; )

Finally, I would discourage storing state about the view (viewAppeared) like in your most recent comment. It will inevitably lead to bugs. In SwiftUI you should always drive your views using your model. The onAppear and related modifiers allow you to respond to events, but a view shouldn't be different based on whether or not it is currently on screen.
@DelawareMathGuy: Thank you for sharing your project. Good to have this well-elaborated project as a reference.

@jjatie: Thank you for the example code with the Notification publisher for obtaining information of a persistence object deletion. It looks really good and seems to be best-suited for my app to handle a View dismiss when I receive a notification after saving the deletion of an object. However, I run into a problem and I hope you can further help me here.

My app has multiple Child-Detail-Views and I want that all Detail-Views are dismissed, if a persistence object was deleted, which these views need. But, I am not able to dismiss all Child-Detail-Views, but only the last one.
I also tried to send my own custom notification independent of the Core-Data delete and save operations -- this has worked. However, if I try to dismiss my Child-Views after deleting my persistence object my Views are not dismissed. :-(
I spent time to build a small example project that shows the problem.

First, I created a new Xcode Project with Core Data enabled. I modified the existing Item entity just a little bit, by adding a name attribute of type String. This is the code of my app @main entry point:
Code Block swift
import SwiftUI
import CoreData
@main
struct SwiftUI_CoreData_ExApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
TabView {
View1().tabItem {
Image(systemName: "1.square.fill")
Text("Tab 1")
}
View1().tabItem {
Image(systemName: "2.square.fill")
Text("Tab 2")
}
}
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.onAppear(perform: {
let moc = persistenceController.container.viewContext
/*Create persistence instances in Core Data database for test and reproduction purpose*/
print("Preparing test data")
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: Item.entity().name!)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
try! moc.execute(deleteRequest)
for i in 1..<4 {
let item = Item(context: moc)
item.name = "Item \(i)"
}
try! moc.save()
})
}
}
}

As you can see I have 2 Tabs using the same view View1. For reproduction and test purposes I add 3 Item instances into the Core Data database onAppear, after deleting already existing ones.

My example project consists of 3 views View1, View2, View3.
When clicking on one of the Tabs users start in the first View and can the navigate via NavigationLink to the second View and then to the third View. This is the code of the View1:
Code Block swift
import SwiftUI
struct View1: View {
@FetchRequest(entity: Item.entity(), sortDescriptors: [])
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(self.items, id: \.self) { item in
View1_Row(item: item)
}
}.listStyle(InsetGroupedListStyle())
.navigationTitle("View 1")
}
}
}
struct View1_Row: View {
@ObservedObject var item: Item
@State var isView2Presented: Bool = false
var body: some View {
NavigationLink(
destination: View2(item: item, isView2Presented: $isView2Presented),
isActive: $isView2Presented,
label: {
Text("\(item.name ?? "missing item name") - View 2")
})
.isDetailLink(false)
}
}

My View1 fetches all instances of Item with @FetchRequeset and renders a NavigationLink for every persistent object. Note that I pass my View2 the actual persistence instance and a Binding (isView2Presented) whether the View2 should be shown resp. is active.
Last reply continued:
This is the code of View2:
Code Block swift
import SwiftUI
struct View2: View {
@Environment(\.managedObjectContext) var moc
@ObservedObject var item: Item
@Binding var isView2Presented: Bool
var body: some View {
List {
Text("Item name: \(item.name ?? "item name unknown")")
View2_Row(item: item)
Button(action: { isView2Presented = false }, label: {Text("Dismiss")})
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("View 2")
.onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "Reset"))) { _ in
print("\(Self.self) inside reset notification closure")
self.isView2Presented = false
}
.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: self.moc),
perform: dismissIfObjectIsDeleted(_:))
}
private func dismissIfObjectIsDeleted(_ notification: Notification) {
if notification.isDeletion(of: self.item) {
print("\(Self.self) dismissIfObjectIsDeleted Dismiss view after deletion of Item")
isView2Presented = false
}
}
}
struct View2_Row : View {
@ObservedObject var item: Item
@State private var isView3Presented: Bool = false
var body: some View {
NavigationLink("View 3",
destination: View3(item: item,
isView3Presented: $isView3Presented),
isActive: $isView3Presented)
.isDetailLink(false)
}
}

In View2 I display one NavigationLink to my View3 and also pass a Binding and the persistent Item instance. This View also listens to my custom Notification named Reset and for .NSManagedObjectContextDidSave and executes a closure when triggered onReceive. On receipt of a notification I want to dismiss the View2, such that View1 is displayed. This is the code of View3:
Code Block swift
import SwiftUI
struct View3: View {
@Environment(\.managedObjectContext) var moc
@ObservedObject var item: Item
@State var isAddViewPresented: Bool = false
@Binding var isView3Presented: Bool
var body: some View {
Group {
List {
Text("Item name: \(item.name ?? "item name unknown")")
Button("DELETE this Item") {
moc.delete(self.item)
try! moc.save()
/*adding the next line does not matter:*/
/*NotificationCenter.default.post(Notification.init(name: Notification.Name(rawValue: "Reset")))*/
}.foregroundColor(.red)
Button(action: {
NotificationCenter.default.post(Notification.init(name: Notification.Name(rawValue: "Reset")))
}, label: {Text("Reset")}).foregroundColor(.green)
Button(action: {isView3Presented = false }, label: {Text("Dismiss")})
}
}
.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: self.moc),
perform: dismissIfObjectIsDeleted(_:))
.onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "Reset"))) { _ in
print("\(Self.self) inside reset notification closure")
self.isView3Presented = false
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("View 3")
.toolbar {
ToolbarItem {
Button(action: {isAddViewPresented.toggle()}, label: {
Label("Add", systemImage: "plus.circle.fill")
})
}
}
.sheet(isPresented: $isAddViewPresented, content: {
Text("DestinationDummyView")
})
}
private func dismissIfObjectIsDeleted(_ notification: Notification) {
if notification.isDeletion(of: self.item) {
print("\(Self.self) dismissIfObjectIsDeleted Dismiss view after deletion of Item")
isView3Presented = false
}
}
}

My View3 also listens for my custom Reset and for the NSManagedObjectContextDidSave notifications, like View2. On receipt of these notifications I want also that View3 gets dismissed. This View has a "Reset" Button, that fires the Reset Notification. A second button ("DELETE this Item") calls delete on the persistence instance Item and save on the NSManagedObjectContext -- which fires the .NSManagedObjectContextDidSave notification.
Last reply continued:
I adjusted your code a bit where you check the deleted objectIDs inside a Notification userInfo, because my notification used the key deleted -- so I had to use NSManagedObjectContext.NotificationKey.deletedObjects.
I wrote an extension of Notification for reusing the check in my second and third View. This is the code of the Notification extension:
Code Block swift
import CoreData
extension Notification {
/*Returns whether this notification is about the deletion of the given `NSManagedObject` instance*/
func isDeletion(of managedObject: NSManagedObject) -> Bool {
guard let deletedObjectIDs = self.deletedObjectIDs
else {
return false
}
return deletedObjectIDs.contains(managedObject.objectID)
}
private var deletedObjectIDs: [NSManagedObjectID]? {
guard let deletedObjects =
self.userInfo?[NSManagedObjectContext.NotificationKey.deletedObjects.rawValue]
as? Set<NSManagedObject>,
deletedObjects.count > 0
else {
return .none
}
return deletedObjects.map(\.objectID)
}
}


What I expect to happen:
If a user navigates from View 1 to View 3 and taps on the Button "DELETE this Item", I want that my Detail-Views get dismissed. I want that View 3 and View 2 get dismissed. I want to return to my first view. This should also work when a user has open View 3 from Tab 1 and Tab 2.

What actually happens:
Dismissing my View 2 and View 3 manually by clicking on the "Dismiss" button works fine. Also the "Reset" Button on View 3 works in a way that View 3 and View 2 are dismissed and I turn back to View 1.
However, as soon as I invoke the delete and save Core Data operation, my Views are not dismissed. Instead, just my View 3 gets dismissed and I am stuck at View 2, where I even cannot manually dismiss the View 2 with the "Dismiss" Button. View 2 cannot access attributes of the deleted NSManagedObject, so instead my fallback values are used.

I also tried to use the environment variable @Environment(\.presentationMode) var presentationMode and presentationMode.wrappedValue.dismiss() instead of Bindings between my Views, but that resulted in the same unwanted behavior.

I would like that all my Child-Detail-Views get dismissed on Deletion resp. firing of Notification. It does not make sense to display View 2 if it is not possible, due to the deletion of my persistence object.

I hope that someone can help me with this issue. I am really stuck here, and I just want to get it to work as I expect it.
Thank you so much for your help! 🙏🏻☺️

Best,
Bernhard
SwiftUI update of deleted CoreData entities from different Tab
 
 
Q