来自 @Environment var Changes 的 ContentView 更新不一致
Inconsistent ContentView updates from @Environment var Changes
我有一个简单的 SwiftUI、CoreData 应用程序。该体系结构是基本列表
用于查看详细信息或编辑详细信息的第二个视图。基本结构好像
与一个重要的例外一起工作。编辑记录时,先编辑后
在 return 进入 ContentView 列表后应用程序启动正确可见。第二个和
return访问 ContentView 时,进一步的编辑不会出现在列表中。数据库
更改已正确保存。重新启动应用程序将显示正确的数据。我有
还创建了一个 TabView。如果我禁用 return-to-main-list-after-edit 代码并使用
只需切换 TabView,始终显示更改。
这是代码(我已经删除了大部分重复的数据字段)。
在 SceneDelegate 中:
let managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let tabby = TabBar().environment(\.managedObjectContext, managedObjectContext)
window.rootViewController = UIHostingController(rootView: tabby)
在内容视图中:
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(fetchRequest: ToDoItem.getAllToDoItems()) var toDoItems: FetchedResults<ToDoItem>
@State private var newToDoItem = ""
@State private var gonnaShow = true
@State private var show = false
var body: some View {
NavigationView {
List {
Section(header: Text("Records")) {
ForEach(self.toDoItems) { toDoItem in
NavigationLink(destination: EditToDo(toDoItem: toDoItem)) {
ToDoItemView(
idString: toDoItem.myID.uuidString,
title: toDoItem.title!,
firstName: toDoItem.firstName!,
lastName: toDoItem.lastName!,
createdAt: self.localTimeString(date: toDoItem.createdAt!)
)
}
}//for each
.onDelete { indexSet in
let deleteItem = self.toDoItems[indexSet.first!]
self.managedObjectContext.delete(deleteItem)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
.onMove(perform: move)
}
}
.navigationBarTitle("Customers")
.navigationBarItems(trailing: EditButton())
}
}
单独的文件 EditToDo:
@Environment(\.managedObjectContext) var managedObjectContext
@Environment(\.presentationMode) var presentationMode
var toDoItem: ToDoItem
@State private var updatedTitle: String = "No Title"
@State private var updatedFirstName: String = "No First Name"
//more data fields
@State private var updatedDate: Date = Date()
@State private var updatedDateString: String = "July 2019"
var body: some View {
ScrollView {
VStack {
Image("JohnForPosting")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80)
.clipShape(Circle())
VStack(alignment: .leading) {
Text("ToDo Title:")
.padding(.leading, 5)
.font(.headline)
TextField("Enter a Title", text: $updatedTitle)
.onAppear {
self.updatedTitle = self.toDoItem.title ?? ""
}
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(10)
VStack(alignment: .leading) {
Text("First Name:")
.padding(.leading, 5)
.font(.headline)
TextField("Enter a First Name", text: $updatedFirstName)
.onAppear {
self.updatedFirstName = self.toDoItem.firstName ?? ""
}
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(10)
//more data fields
VStack {
Button(action: ({
self.toDoItem.title = self.updatedTitle
self.toDoItem.firstName = self.updatedFirstName
//more data fields
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
self.updatedTitle = ""
self.updatedFirstName = ""
//more data fields
self.presentationMode.wrappedValue.dismiss()
})) {
Text("Save")
}
.padding()
}
.padding(10)
Spacer()
}
}
}
单独文件 ToDoItemView:
var idString: String = ""
var title: String = ""
var firstName: String = ""
//more data fields
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("\(firstName) \(lastName)")
.font(.headline)
Text("\(createdAt) and \(idString)")
.font(.caption)
}
}
}
Xcode 11 - 我猜这是真正的版本(post GM seed 2),Catalina Beta 19A558d,
iOS13.1
我认为 @Environment 的变化总是会导致 ContentView 的主体是
重新绘制。这是第一次编辑工作的奇怪行为,其他人则没有。任何指导
将不胜感激。
我现在的项目也有同样的问题,基本代码也和你一样。由导航到第二个视图以编辑项目的 FetchRequest 提供的项目列表。
像你一样,如果我在详细视图中进行更改,这些更改会反映在列表中,但只是第一次。如果我通过切换到不同的选项卡并再次返回来重新加载列表,列表将更新显示更新的数据。
问题的根源归结为使用@FetchRequest 的结果创建动态列表。 FetchRequest returns 一组 NSManagedObjects(引用类型)。由于 List 以一组引用类型为种子,SwiftUI 只会在引用更改时更新视图,不一定在对象的一个属性更改时更新视图。
我认为 @FetchRequest 适用于选择器或菜单选项之类的东西,但对于 TodoItems 的动态列表,更好的解决方案可能是创建 TodoManager。
TodoManager 将是所有 TodoItem 的唯一真实来源。它还将设置一个 NSManagedObjectContextObserver,在进行任何更改时触发 objectWillChange.send() 通知,从而表明 SwiftUI 应该刷新列表。
class TodoManager: ObservableObject {
private var moc: NSManagedObjectContext
public let objectWillChange = PassthroughSubject<Void, Never>()
var todoList: [TodoItem] = []
init( context: NSManagedObjectContext ) {
moc = context
setupNSManagedObjectContextObserver( context: context )
todoList = Array( TodoItem.fetch(in: context ))
}
func setupNSManagedObjectContextObserver( context moc: NSManagedObjectContext ) {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(managedObjectContextObjectsDidChange), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: moc)
notificationCenter.addObserver(self, selector: #selector(managedObjectContextWillSave), name: NSNotification.Name.NSManagedObjectContextWillSave, object: moc)
notificationCenter.addObserver(self, selector: #selector(managedObjectContextDidSave), name: NSNotification.Name.NSManagedObjectContextDidSave, object: moc)
}
@objc func managedObjectContextObjectsDidChange(notification: NSNotification) {
self.objectWillChange.send() // Crude but works
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
@objc func managedObjectContextWillSave(notification: NSNotification) {
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
@objc func managedObjectContextDidSave(notification: NSNotification) {
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
}
TodoManager 非常愚蠢,因为它会在 ManagedObjectContext 中的任何数据发生更改时发送 objectWillChange 通知。
史蒂夫
Xcode 11.1 GM 种子解决了这个问题。值得一提的是,核心数据馈送列表的预览功能仍然不起作用。
我有一个简单的 SwiftUI、CoreData 应用程序。该体系结构是基本列表 用于查看详细信息或编辑详细信息的第二个视图。基本结构好像 与一个重要的例外一起工作。编辑记录时,先编辑后 在 return 进入 ContentView 列表后应用程序启动正确可见。第二个和 return访问 ContentView 时,进一步的编辑不会出现在列表中。数据库 更改已正确保存。重新启动应用程序将显示正确的数据。我有 还创建了一个 TabView。如果我禁用 return-to-main-list-after-edit 代码并使用 只需切换 TabView,始终显示更改。
这是代码(我已经删除了大部分重复的数据字段)。
在 SceneDelegate 中:
let managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let tabby = TabBar().environment(\.managedObjectContext, managedObjectContext)
window.rootViewController = UIHostingController(rootView: tabby)
在内容视图中:
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(fetchRequest: ToDoItem.getAllToDoItems()) var toDoItems: FetchedResults<ToDoItem>
@State private var newToDoItem = ""
@State private var gonnaShow = true
@State private var show = false
var body: some View {
NavigationView {
List {
Section(header: Text("Records")) {
ForEach(self.toDoItems) { toDoItem in
NavigationLink(destination: EditToDo(toDoItem: toDoItem)) {
ToDoItemView(
idString: toDoItem.myID.uuidString,
title: toDoItem.title!,
firstName: toDoItem.firstName!,
lastName: toDoItem.lastName!,
createdAt: self.localTimeString(date: toDoItem.createdAt!)
)
}
}//for each
.onDelete { indexSet in
let deleteItem = self.toDoItems[indexSet.first!]
self.managedObjectContext.delete(deleteItem)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
.onMove(perform: move)
}
}
.navigationBarTitle("Customers")
.navigationBarItems(trailing: EditButton())
}
}
单独的文件 EditToDo:
@Environment(\.managedObjectContext) var managedObjectContext
@Environment(\.presentationMode) var presentationMode
var toDoItem: ToDoItem
@State private var updatedTitle: String = "No Title"
@State private var updatedFirstName: String = "No First Name"
//more data fields
@State private var updatedDate: Date = Date()
@State private var updatedDateString: String = "July 2019"
var body: some View {
ScrollView {
VStack {
Image("JohnForPosting")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80)
.clipShape(Circle())
VStack(alignment: .leading) {
Text("ToDo Title:")
.padding(.leading, 5)
.font(.headline)
TextField("Enter a Title", text: $updatedTitle)
.onAppear {
self.updatedTitle = self.toDoItem.title ?? ""
}
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(10)
VStack(alignment: .leading) {
Text("First Name:")
.padding(.leading, 5)
.font(.headline)
TextField("Enter a First Name", text: $updatedFirstName)
.onAppear {
self.updatedFirstName = self.toDoItem.firstName ?? ""
}
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(10)
//more data fields
VStack {
Button(action: ({
self.toDoItem.title = self.updatedTitle
self.toDoItem.firstName = self.updatedFirstName
//more data fields
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
self.updatedTitle = ""
self.updatedFirstName = ""
//more data fields
self.presentationMode.wrappedValue.dismiss()
})) {
Text("Save")
}
.padding()
}
.padding(10)
Spacer()
}
}
}
单独文件 ToDoItemView:
var idString: String = ""
var title: String = ""
var firstName: String = ""
//more data fields
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("\(firstName) \(lastName)")
.font(.headline)
Text("\(createdAt) and \(idString)")
.font(.caption)
}
}
}
Xcode 11 - 我猜这是真正的版本(post GM seed 2),Catalina Beta 19A558d, iOS13.1 我认为 @Environment 的变化总是会导致 ContentView 的主体是 重新绘制。这是第一次编辑工作的奇怪行为,其他人则没有。任何指导 将不胜感激。
我现在的项目也有同样的问题,基本代码也和你一样。由导航到第二个视图以编辑项目的 FetchRequest 提供的项目列表。
像你一样,如果我在详细视图中进行更改,这些更改会反映在列表中,但只是第一次。如果我通过切换到不同的选项卡并再次返回来重新加载列表,列表将更新显示更新的数据。
问题的根源归结为使用@FetchRequest 的结果创建动态列表。 FetchRequest returns 一组 NSManagedObjects(引用类型)。由于 List 以一组引用类型为种子,SwiftUI 只会在引用更改时更新视图,不一定在对象的一个属性更改时更新视图。
我认为 @FetchRequest 适用于选择器或菜单选项之类的东西,但对于 TodoItems 的动态列表,更好的解决方案可能是创建 TodoManager。
TodoManager 将是所有 TodoItem 的唯一真实来源。它还将设置一个 NSManagedObjectContextObserver,在进行任何更改时触发 objectWillChange.send() 通知,从而表明 SwiftUI 应该刷新列表。
class TodoManager: ObservableObject {
private var moc: NSManagedObjectContext
public let objectWillChange = PassthroughSubject<Void, Never>()
var todoList: [TodoItem] = []
init( context: NSManagedObjectContext ) {
moc = context
setupNSManagedObjectContextObserver( context: context )
todoList = Array( TodoItem.fetch(in: context ))
}
func setupNSManagedObjectContextObserver( context moc: NSManagedObjectContext ) {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(managedObjectContextObjectsDidChange), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: moc)
notificationCenter.addObserver(self, selector: #selector(managedObjectContextWillSave), name: NSNotification.Name.NSManagedObjectContextWillSave, object: moc)
notificationCenter.addObserver(self, selector: #selector(managedObjectContextDidSave), name: NSNotification.Name.NSManagedObjectContextDidSave, object: moc)
}
@objc func managedObjectContextObjectsDidChange(notification: NSNotification) {
self.objectWillChange.send() // Crude but works
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
@objc func managedObjectContextWillSave(notification: NSNotification) {
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
@objc func managedObjectContextDidSave(notification: NSNotification) {
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
}
TodoManager 非常愚蠢,因为它会在 ManagedObjectContext 中的任何数据发生更改时发送 objectWillChange 通知。
史蒂夫
Xcode 11.1 GM 种子解决了这个问题。值得一提的是,核心数据馈送列表的预览功能仍然不起作用。