Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.8k views
in Technique[技术] by (71.8m points)

ios - Cannot dismiss multiple Detail-Views after CoreData save in SwiftUI

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.

Cannot Dismiss multiple Detail-Views after Core Data save

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.

Xcode datamodel showing Entities

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()
            })
        }
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)
等待大神答复

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...