首页 > 解决方案 > 使用 @ObservedObject 包装的核心数据对象会导致 NavigationLink 目标在该对象被删除时重新呈现

问题描述

我遇到了一个问题,即 SwiftUI 正在为刚刚删除的 Core Data 对象呈现视图。我已经使用 Xcode 提供的基本 SwiftUI+Core Data 模板重现了这个问题。

import SwiftUI
import CoreData

struct ContentView: View {
  @State var selectedItem: Item?
  @Environment(\.managedObjectContext) private var managedObjectContext
  
  @FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
    animation: .default)
  private var items: FetchedResults<Item>
  
  var body: some View {
    NavigationView {
      VStack {
        List {
          ForEach(items) { item in
            NavigationLink(
              destination: DetailView(item: item).environment(\.managedObjectContext, managedObjectContext),
              tag: item,
              selection: $selectedItem
            ){
              Text("Item at \(item.timestamp!, formatter: itemFormatter)")
            }
          }
          .onDelete(perform: deleteItems)
        }
      }
      .navigationTitle("Items")
      .toolbar {
        Button(action: addItem) {
          Label("Add Item", systemImage: "plus")
        }
      }
    }
  }
  
  private func addItem() {
    withAnimation {
      let newItem = Item(context: managedObjectContext)
      newItem.timestamp = Date()
      
      do {
        try managedObjectContext.save()
      } catch {
        // Replace this implementation with code to handle the error appropriately.
        // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
        let nsError = error as NSError
        fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
      }
    }
  }
  
  private func deleteItems(offsets: IndexSet) {
    withAnimation {
      offsets.map { items[$0] }.forEach(managedObjectContext.delete)
      
      do {
        try managedObjectContext.save()
      } catch {
        // Replace this implementation with code to handle the error appropriately.
        // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
        let nsError = error as NSError
        fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
      }
    }
  }
}

struct DetailView: View {
  @ObservedObject var item: Item
  @Environment(\.managedObjectContext) var managedObjectContext
  @State var show = false
  
  var body: some View {
    Text("Detail item at \(item.timestamp!, formatter: itemFormatter)")
      .navigationTitle("Detail")
      .toolbar {
        Button {
          show = true
        } label: {
          Text("Edit")
        }
      }
      .sheet(isPresented: $show) {
        Popup(item: item)
          .environment(\.managedObjectContext, managedObjectContext)
      }
  }
}

struct Popup: View {
  var item: Item
  @Environment(\.managedObjectContext) var managedObjectContext
  @Environment(\.presentationMode) var presentationMode
  
  var body: some View {
    VStack {
      Text("Popup item at \(item.timestamp!, formatter: itemFormatter)")
      Button {
        item.timestamp = Calendar.current.date(byAdding: .second, value: 1, to: item.timestamp!)!
        try! managedObjectContext.save()
        presentationMode.wrappedValue.dismiss()
      } label: {
        Text("Add second and close")
      }
    }
  }
}

private let itemFormatter: DateFormatter = {
  let formatter = DateFormatter()
  formatter.dateStyle = .short
  formatter.timeStyle = .medium
  return formatter
}()

struct PersistenceController {
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        for _ in 0..<10 {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
        }
        do {
            try viewContext.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "NavigationLinkDelete")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

                /*
                Typical reasons for an error here include:
                * The parent directory does not exist, cannot be created, or disallows writing.
                * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                * The device is out of space.
                * The store could not be migrated to the current model version.
                Check the error message to determine what the actual problem was.
                */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
  }
}

总而言之,ContentView从 Core Data 中检索一些项目并将它们呈现在一个列表中。如果用户点击一个,则NavigationLink显示DetailView. 然后用户可以点击一个编辑按钮来打开一个Popup用户可以编辑项目的工作表(在这种情况下,用户只能在Item正在编辑的核心数据上添加一秒钟。)

如果用户首先点击一个项目List以查看详细视图,向后导航以List再次查看,滑动他们之前点击的列表项目,然后点击Delete,该项目将被删除,但应用程序将在DetailView正文中崩溃正在尝试检索timestamp项目。

问题在于使用属性包装器DetailView观察。我这样做是因为当用户编辑 中的项目并且该工作表被关闭时,我希望自动更新。如果我删除它,那么执行上述步骤不会导致崩溃,但是当它被解雇时不会更新。Item@ObservedObjectPopupDetailView@ObservedObjectDetailViewPopup

我相信因为DetailView正在观察Item, 当用户删除时Item它会导致 lastDetailView的身体重新渲染,尽管事实上NavigationView没有显示 anItem的细节。DetailView在用户滑动以删除该行时, 不可见,List因此尚不清楚为什么会发生这种情况。

我可以通过检查's来缓解这个问题,item.isFault如果它是假的,则只访问字段。不过,这似乎有点骇人听闻。DetailViewbody

这是一个 SwiftUI 错误吗?有没有更好的方法来实现我想要的?

标签: swiftcore-dataswiftui

解决方案


在我学习的过程中,我一直在使用 SwiftUI 和 CoreData 进行删除时崩溃。我在行视图实现和它显示的 iPad 上的详细视图中体验过它。

我看到很多关于这个问题的讨论,但我认为你的“hack”可能是最好的。我此时的解决方案偏好:

  1. 使用 ObservedObject 并检查是否item.isFault显示占位符或空视图。

  2. 如果可以,请let在视图不需要响应更改并且可以随着FetchRequest更改传播而消失的情况下使用模型对象。

  3. 做一些更复杂的事情,将对象标记为将来要删除,并将已删除的对象从获取中过滤掉。这样它就不会突然消失并导致崩溃。您可能想使用垃圾并最终需要清理它。

SwiftUI 似乎最好延迟更新已删除模型对象的视图,但这对我来说是全新的。

这是一个常见问题解答,但我没有看到一个常见的答案。


推荐阅读