Post

Replies

Boosts

Views

Activity

Reply to SwiftUI update of deleted CoreData entities from different Tab
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: import CoreData extension Notification { &#9;&#9;/*Returns whether this notification is about the deletion of the given `NSManagedObject` instance*/ &#9;&#9;func isDeletion(of managedObject: NSManagedObject) -> Bool { &#9;&#9;&#9;&#9;guard let deletedObjectIDs = self.deletedObjectIDs &#9;&#9;&#9;&#9;else { &#9;&#9;&#9;&#9;&#9;&#9;return false &#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;return deletedObjectIDs.contains(managedObject.objectID) &#9;&#9;} &#9;&#9;private var deletedObjectIDs: [NSManagedObjectID]? { &#9;&#9;&#9;&#9;guard let deletedObjects = &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;self.userInfo?[NSManagedObjectContext.NotificationKey.deletedObjects.rawValue] &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;as? Set<NSManagedObject>, &#9;&#9;&#9;&#9;&#9;&#9;&#9;deletedObjects.count > 0 &#9;&#9;&#9;&#9;else { &#9;&#9;&#9;&#9;&#9;&#9;return .none &#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;return deletedObjects.map(\.objectID) &#9;&#9;} } 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
Jan ’21
Reply to SwiftUI update of deleted CoreData entities from different Tab
Last reply continued: This is the code of View2: import SwiftUI struct View2: View { &#9;&#9;@Environment(\.managedObjectContext) var moc &#9;&#9;@ObservedObject var item: Item &#9;&#9;@Binding var isView2Presented: Bool &#9;&#9; &#9;&#9;var body: some View { &#9;&#9;&#9;&#9;List { &#9;&#9;&#9;&#9;&#9;&#9;Text("Item name: \(item.name ?? "item name unknown")") &#9;&#9;&#9;&#9;&#9;&#9;View2_Row(item: item) &#9;&#9;&#9;&#9;&#9;&#9;Button(action: { isView2Presented = false }, label: {Text("Dismiss")}) &#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;.listStyle(InsetGroupedListStyle()) &#9;&#9;&#9;&#9;.navigationTitle("View 2") &#9;&#9;&#9;&#9;.onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "Reset"))) { _ in &#9;&#9;&#9;&#9;&#9;&#9;print("\(Self.self) inside reset notification closure") &#9;&#9;&#9;&#9;&#9;&#9;self.isView2Presented = false &#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: self.moc), &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9; perform: dismissIfObjectIsDeleted(_:)) &#9;&#9;} &#9;&#9; &#9;&#9;private func dismissIfObjectIsDeleted(_ notification: Notification) { &#9;&#9;&#9;&#9;if notification.isDeletion(of: self.item) { &#9;&#9;&#9;&#9;&#9;&#9;print("\(Self.self) dismissIfObjectIsDeleted Dismiss view after deletion of Item") &#9;&#9;&#9;&#9;&#9;&#9;isView2Presented = false &#9;&#9;&#9;&#9;} &#9;&#9;} } struct View2_Row : View { &#9;&#9;@ObservedObject var item: Item &#9;&#9;@State private var isView3Presented: Bool = false &#9;&#9;&#9;&#9; &#9;&#9;var body: some View { &#9;&#9;&#9;&#9;NavigationLink("View 3", &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9; destination: View3(item: item, &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;isView3Presented: $isView3Presented), &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9; isActive: $isView3Presented) &#9;&#9;&#9;&#9;.isDetailLink(false) &#9;&#9;} } 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: import SwiftUI struct View3: View { &#9;&#9;@Environment(\.managedObjectContext) var moc &#9;&#9;@ObservedObject var item: Item &#9;&#9; &#9;&#9;@State var isAddViewPresented: Bool = false &#9;&#9;@Binding var isView3Presented: Bool &#9;&#9;var body: some View { &#9;&#9;&#9;&#9;Group { &#9;&#9;&#9;&#9;&#9;&#9;List { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Text("Item name: \(item.name ?? "item name unknown")") &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9; &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Button("DELETE this Item") { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;moc.delete(self.item) &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;try! moc.save() &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;/*adding the next line does not matter:*/ &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;/*NotificationCenter.default.post(Notification.init(name: Notification.Name(rawValue: "Reset")))*/ &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;}.foregroundColor(.red) &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9; &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Button(action: { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;NotificationCenter.default.post(Notification.init(name: Notification.Name(rawValue: "Reset"))) &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;}, label: {Text("Reset")}).foregroundColor(.green) &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9; &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Button(action: {isView3Presented = false }, label: {Text("Dismiss")}) &#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: self.moc), &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9; perform: dismissIfObjectIsDeleted(_:)) &#9;&#9;&#9;&#9;.onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "Reset"))) { _ in &#9;&#9;&#9;&#9;&#9;&#9;print("\(Self.self) inside reset notification closure") &#9;&#9;&#9;&#9;&#9;&#9;self.isView3Presented = false &#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;.listStyle(InsetGroupedListStyle()) &#9;&#9;&#9;&#9;.navigationTitle("View 3") &#9;&#9;&#9;&#9;.toolbar { &#9;&#9;&#9;&#9;&#9;&#9;ToolbarItem { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Button(action: {isAddViewPresented.toggle()}, label: { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Label("Add", systemImage: "plus.circle.fill") &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;}) &#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;.sheet(isPresented: $isAddViewPresented, content: { &#9;&#9;&#9;&#9;&#9;&#9;Text("DestinationDummyView") &#9;&#9;&#9;&#9;}) &#9;&#9;} &#9;&#9; &#9;&#9;private func dismissIfObjectIsDeleted(_ notification: Notification) { &#9;&#9;&#9;&#9;if notification.isDeletion(of: self.item) { &#9;&#9;&#9;&#9;&#9;&#9;print("\(Self.self) dismissIfObjectIsDeleted Dismiss view after deletion of Item") &#9;&#9;&#9;&#9;&#9;&#9;isView3Presented = false &#9;&#9;&#9;&#9;} &#9;&#9;} } 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.
Jan ’21
Reply to SwiftUI update of deleted CoreData entities from different Tab
@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: import SwiftUI import CoreData @main struct SwiftUI_CoreData_ExApp: App { &#9;&#9;let persistenceController = PersistenceController.shared &#9;&#9;var body: some Scene { &#9;&#9;&#9;&#9;WindowGroup { &#9;&#9;&#9;&#9;&#9;&#9;TabView { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;View1().tabItem { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Image(systemName: "1.square.fill") &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Text("Tab 1") &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;View1().tabItem { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Image(systemName: "2.square.fill") &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Text("Tab 2") &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;&#9;&#9;.environment(\.managedObjectContext, persistenceController.container.viewContext) &#9;&#9;&#9;&#9;&#9;&#9;.onAppear(perform: { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;let moc = persistenceController.container.viewContext &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;/*Create persistence instances in Core Data database for test and reproduction purpose*/ &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;print("Preparing test data") &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: Item.entity().name!) &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;try! moc.execute(deleteRequest) &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;for i in 1..<4 { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;let item = Item(context: moc) &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;item.name = "Item \(i)" &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;try! moc.save() &#9;&#9;&#9;&#9;&#9;&#9;}) &#9;&#9;&#9;&#9;} &#9;&#9;} } 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: import SwiftUI struct View1: View { &#9;&#9;@FetchRequest(entity: Item.entity(), sortDescriptors: []) &#9;&#9;private var items: FetchedResults<Item> &#9;&#9; &#9;&#9;var body: some View { &#9;&#9;&#9;&#9;NavigationView { &#9;&#9;&#9;&#9;&#9;&#9;List { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;ForEach(self.items, id: \.self) { item in &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;View1_Row(item: item) &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;&#9;&#9;}.listStyle(InsetGroupedListStyle()) &#9;&#9;&#9;&#9;&#9;&#9;.navigationTitle("View 1") &#9;&#9;&#9;&#9;} &#9;&#9;} } struct View1_Row: View { &#9;&#9;@ObservedObject var item: Item &#9;&#9;@State var isView2Presented: Bool = false &#9;&#9;var body: some View { &#9;&#9;&#9;&#9;NavigationLink( &#9;&#9;&#9;&#9;&#9;&#9;destination: View2(item: item, isView2Presented: $isView2Presented), &#9;&#9;&#9;&#9;&#9;&#9;isActive: $isView2Presented, &#9;&#9;&#9;&#9;&#9;&#9;label: { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Text("\(item.name ?? "missing item name") - View 2") &#9;&#9;&#9;&#9;&#9;&#9;}) &#9;&#9;&#9;&#9;&#9;&#9;.isDetailLink(false) &#9;&#9;} } 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.
Jan ’21
Reply to SwiftUI update of deleted CoreData entities from different Tab
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: struct DetailView: View {     @State private var viewAppeared: Bool = false &#9;&#9;var body: some View { &#9;&#9;&#9;&#9;Group { &#9;&#9;&#9;&#9;&#9;&#9;RealDetailView() &#9;&#9;&#9;&#9;&#9;&#9;if viewAppeared {                 EmptyView() &#9;&#9;&#9;&#9;}.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. :-)
Jan ’21
Reply to SwiftUI update of deleted CoreData entities from different Tab
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: struct Tab1: View { &#9;&#9;@Environment(\.managedObjectContext) var managedObjectContext &#9;&#9;@FetchRequest(entity: MyPersistenceObject.entity(),                   sortDescriptors: [])     var myPersistenceObjects: FetchedResults<MyPersistenceObject> &#9;&#9;... &#9;&#9;ForEach(myPersistenceObjects, id: \.self) { myPersistenceObject in &#9;&#9;&#9;&#9; ListViewTab1(myPersistenceObject: myPersistenceObject) &#9;&#9;} &#9;&#9; 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. 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 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. struct Detail-View2-Parent: View {     @Environment(\.managedObjectContext) var managedObjectContext     @ObservedObject     var myPersistenceObjectParent: MyPersistenceObjectParent &#9;&#9;var body: some View {         List {                 ForEach(myPersistenceObjectParent.myPersistenceObjects, id: \.self) { myPersistenceObject in                     DetailView(myPersistenceObject: myPersistenceObject)                 } Sorry, that its not really clearly structured. :(
Jan ’21
Reply to Dismiss of SwiftUI Views one after the other programmatically does not work
My question is resolved. I got help on StackOverFlow [1]. In short: The solution is to add ".isDetailLink(false)" to each NavigationLink in my views. You may see the Apple Documentation on "isDetailLink(_:)" [2]. &#9;&#9;NavigationLink( &#9;&#9;&#9;&#9;destination: View2(types: ["Exams"], presentView2: $presentView2), &#9;&#9;&#9;&#9;isActive: $presentView2, &#9;&#9;&#9;&#9;label: { &#9;&#9;&#9;&#9;&#9;&#9;Text("View2") &#9;&#9;&#9;&#9;}).isDetailLink(false) ... &#9;&#9;NavigationLink("View3", destination: View3(presentView3: $presentView3), isActive: $presentView3) &#9;&#9;&#9; .isDetailLink(false) ... &#9;&#9;NavigationLink("View4", destination: View4(presentView4: $presentView4), isActive: $presentView4) &#9;&#9;&#9; .isDetailLink(false) [1] https://stackoverflow.com/questions/64958493/dismiss-of-swiftui-views-one-after-the-other-programmatically-does-not-work [2] https://developer.apple.com/documentation/swiftui/navigationlink/isdetaillink(_:)
Nov ’20