I have an app with multiple Detail Views that use as source instances of NSManagedObject
.
Imagine View 1 fetches all persistent instances of Entity Item
with @FetchRequeset
and displays them in a List View.
When clicking on one item in the list, a second View (Detail-View) is opened.
If a user navigates from View 1 to View 2 a persistence instance is shared with the View 2.
View 2 has a NavigationLink
zu another Detail-View View3. View 2 also shares the persistence instance with View 3.
On View3 a user can click on a Button ("DELETE this Item"), which initiates the deletion of the CoreData persistence instance and a save of the NSManagedObjectContext
.
After saving I want that all my Detail-Views (View2 and View3) are dismissed, and a user returns back to the entry view, View 1 (List-View).
My app listens for Notifications of NSManagedObjectContextDidSave
and sets Bindings for isActive
on NavigationLink
instances to false
. Instead of working with Bindings to dismiss the DetailViews, I also tried to use the presentationMode
environment Variable with self.presentationMode.wrappedValue.dismiss()
.
However, it does not work to dismiss View 2 and View 3. After saving the NSManagedObjectContext
just View 3 gets dismissed and View 2 is stuck and cannot be dismissed.
I hope someone also faces this issue and knows how to solve it. I appreciate any support! Thank you!
1. UPDATE on 13th of January 2020: Let me clarify my post here: My notification closures are executed and Bindings representing whether my Views are presented are also updated. However, my only question here is why my View 2 is not dismissed and stuck, after View 3 has been dismissed. Am I understanding something wrong? My example code is quite big, but for reproducing the issue it needs at least 3 Views (i.e. 2 Detail-Views). With just 1 List and 1 Detail-View the issue will not occur.
The following GIF shows the issue.
I built an example project for reproducibility. 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
. I currently use Xcode 12.2 and iOS 14.2.
This is the SwiftUI code for View 1, View 2 and View 3:
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)
}
}
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)
}
}
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
}
}
}
This is the code of my Notification
extension -- used for checking if the NSManagedObject
is deleted:
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)
}
}
This is the code of my app @main
entry point. It generates example data on app start and my app has 2 Tabs.:
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()
})
}
}
}