swift - 使用 @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
@ObservedObject
Popup
DetailView
@ObservedObject
DetailView
Popup
我相信因为DetailView
正在观察Item
, 当用户删除时Item
它会导致 lastDetailView
的身体重新渲染,尽管事实上NavigationView
没有显示 anItem
的细节。DetailView
在用户滑动以删除该行时, 不可见,List
因此尚不清楚为什么会发生这种情况。
我可以通过检查's来缓解这个问题,item.isFault
如果它是假的,则只访问字段。不过,这似乎有点骇人听闻。DetailView
body
这是一个 SwiftUI 错误吗?有没有更好的方法来实现我想要的?
解决方案
在我学习的过程中,我一直在使用 SwiftUI 和 CoreData 进行删除时崩溃。我在行视图实现和它显示的 iPad 上的详细视图中体验过它。
我看到很多关于这个问题的讨论,但我认为你的“hack”可能是最好的。我此时的解决方案偏好:
使用 ObservedObject 并检查是否
item.isFault
显示占位符或空视图。如果可以,请
let
在视图不需要响应更改并且可以随着FetchRequest
更改传播而消失的情况下使用模型对象。做一些更复杂的事情,将对象标记为将来要删除,并将已删除的对象从获取中过滤掉。这样它就不会突然消失并导致崩溃。您可能想使用垃圾并最终需要清理它。
SwiftUI 似乎最好延迟更新已删除模型对象的视图,但这对我来说是全新的。
这是一个常见问题解答,但我没有看到一个常见的答案。
推荐阅读
- unity3d - Unity 2d 中是否有任何功能,当我死时游戏结束屏幕只有在死亡动画完成后才会出现
- reactjs - FIrebase Google Auth 注销错误连接被拒绝
- python-3.x - numpy-1.19.5 不是此平台上支持的轮子,(RHEL 7.9 64 位)
- excel - 在工作表中插入缺少的列并将数据粘贴到新的 Excel 中
- html - 如何使用css在标题下放置线条和点
- android-studio - 插件错误:插件“Visual Paradigm SDE for IntelliJ IDEA(社区版)”不兼容(仅在 IntelliJ IDEA 中支持)
- c++ - 我可以将矩形范围转换为增强多边形吗?
- apache-superset - Apache Superset 仪表板中有两种选项卡。如何制作适用于整个仪表板的那种?
- excel - 仅通过以输入框的形式选择工作表而不在 VBA 中提及它们的名称,将数据从源工作表动态导入目标工作表
- php - 样式文件不包含在 php laravel 中