在 SwiftUI 中更改对象的子项时刷新视图
Refresh a view when the children of an object are changed in SwiftUI
我正在开发一个包含两个实体 MyList 和 MyListItem 的 CoreData 应用程序。 MyList 可以有多个 MyListItem(一对多)。当应用程序启动时,我可以看到所有列表。我可以点击列表以转到列表项。在该屏幕上,我点击一个按钮将一个项目添加到所选列表中。之后,当我返回所有列表屏幕时添加项目,我看不到计数中反映的项目数。原因是 MyListsView 没有再次渲染,因为列表的数量没有改变。
完整代码如下:
import SwiftUI
import CoreData
extension MyList {
static var all: NSFetchRequest<MyList> {
let request = MyList.fetchRequest()
request.sortDescriptors = []
return request
}
}
struct DetailView: View {
@Environment(\.managedObjectContext) var viewContext
let myList: MyList
var body: some View {
VStack {
Text("Detail View")
Button("Add List Item") {
let myListP = viewContext.object(with: myList.objectID) as! MyList
let myListItem = MyListItem(context: viewContext)
myListItem.name = randomString()
myListItem.myList = myListP
try? viewContext.save()
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
class ViewModel: NSObject, ObservableObject {
@Published var myLists: [MyList] = []
private var fetchedResultsController: NSFetchedResultsController<MyList>
private(set) var context: NSManagedObjectContext
override init() {
self.context = CoreDataManager.shared.context
fetchedResultsController = NSFetchedResultsController(fetchRequest: MyList.all, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
guard let myLists = fetchedResultsController.fetchedObjects else { return }
self.myLists = myLists
} catch {
print(error)
}
}
}
extension ViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let myLists = controller.fetchedObjects as? [MyList] else { return }
self.myLists = myLists
}
}
struct MyListsView: View {
let myLists: [MyList]
var body: some View {
List(myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
struct ContentView: View {
@StateObject private var vm = ViewModel()
@Environment(\.managedObjectContext) var viewContext
var body: some View {
NavigationView {
VStack {
// when adding an item to the list the MyListView view is
// not re-rendered
MyListsView(myLists: vm.myLists)
Button("Change List") {
}
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
在 ContentView 中有一个名为“MyListsView”的视图。添加项目时不会呈现该视图。因为,根据那个观点,没有任何改变,因为列表的数量仍然相同。
你是怎么解决这个问题的?
更新:
如果我为 ListCellView 添加更多级别的视图,会发生什么情况,如下所示:
struct MyListCellView: View {
@StateObject var vm: ListCellViewModel
init(vm: ListCellViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
HStack {
Text(vm.name)
Spacer()
Text("\((vm.items).count)")
}
}
}
@MainActor
class ListCellViewModel: ObservableObject {
let myList: MyList
init(myList: MyList) {
self.myList = myList
self.name = myList.name ?? ""
self.items = myList.items!.allObjects as! [MyListItem]
print(self.items.count)
}
@Published var name: String = ""
@Published var items: [MyListItem] = []
}
struct MyListsView: View {
@StateObject var vm: ViewModel
init(vm: ViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
let _ = Self._printChanges()
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
MyListCellView(vm: ListCellViewModel(myList: myList))
}
}
}
}
现在计数不再更新。
您的 ViewModel
是一个 ObserveableObject
,但您没有在 MyListsView
中观察到它。当你初始化 MyListsView
时,你设置了一个 let 常量。当然不会更新。改为这样做:
struct MyListsView: View {
@ObservedObject var vm: ViewModel
init(viewModel: ViewModel) {
self.vm = viewModel
}
var body: some View {
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
现在 ViewModel
中的 @Published
会导致 MyListView
发生变化,其中包括添加相关实体。
首先摆脱你的视图模型,我们不在 SwiftUI 中使用 MVVM,我们使用视图数据结构来处理我们的视图数据和 属性 包装器使它们表现得像对象。在您的情况下,对列表使用 @FetchRequest
属性 包装器,对细节使用 @ObservedObject
包装器,并且将在对模型数据进行任何更改时调用正文。检查 Xcode 中的应用程序模板中的代码,并检查核心数据。它看起来像这样:
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
DetailView(item: item)
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
...
struct DetailView: View {
@ObservedObject var item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
我正在开发一个包含两个实体 MyList 和 MyListItem 的 CoreData 应用程序。 MyList 可以有多个 MyListItem(一对多)。当应用程序启动时,我可以看到所有列表。我可以点击列表以转到列表项。在该屏幕上,我点击一个按钮将一个项目添加到所选列表中。之后,当我返回所有列表屏幕时添加项目,我看不到计数中反映的项目数。原因是 MyListsView 没有再次渲染,因为列表的数量没有改变。
完整代码如下:
import SwiftUI
import CoreData
extension MyList {
static var all: NSFetchRequest<MyList> {
let request = MyList.fetchRequest()
request.sortDescriptors = []
return request
}
}
struct DetailView: View {
@Environment(\.managedObjectContext) var viewContext
let myList: MyList
var body: some View {
VStack {
Text("Detail View")
Button("Add List Item") {
let myListP = viewContext.object(with: myList.objectID) as! MyList
let myListItem = MyListItem(context: viewContext)
myListItem.name = randomString()
myListItem.myList = myListP
try? viewContext.save()
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
class ViewModel: NSObject, ObservableObject {
@Published var myLists: [MyList] = []
private var fetchedResultsController: NSFetchedResultsController<MyList>
private(set) var context: NSManagedObjectContext
override init() {
self.context = CoreDataManager.shared.context
fetchedResultsController = NSFetchedResultsController(fetchRequest: MyList.all, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
guard let myLists = fetchedResultsController.fetchedObjects else { return }
self.myLists = myLists
} catch {
print(error)
}
}
}
extension ViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let myLists = controller.fetchedObjects as? [MyList] else { return }
self.myLists = myLists
}
}
struct MyListsView: View {
let myLists: [MyList]
var body: some View {
List(myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
struct ContentView: View {
@StateObject private var vm = ViewModel()
@Environment(\.managedObjectContext) var viewContext
var body: some View {
NavigationView {
VStack {
// when adding an item to the list the MyListView view is
// not re-rendered
MyListsView(myLists: vm.myLists)
Button("Change List") {
}
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
在 ContentView 中有一个名为“MyListsView”的视图。添加项目时不会呈现该视图。因为,根据那个观点,没有任何改变,因为列表的数量仍然相同。
你是怎么解决这个问题的?
更新:
如果我为 ListCellView 添加更多级别的视图,会发生什么情况,如下所示:
struct MyListCellView: View {
@StateObject var vm: ListCellViewModel
init(vm: ListCellViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
HStack {
Text(vm.name)
Spacer()
Text("\((vm.items).count)")
}
}
}
@MainActor
class ListCellViewModel: ObservableObject {
let myList: MyList
init(myList: MyList) {
self.myList = myList
self.name = myList.name ?? ""
self.items = myList.items!.allObjects as! [MyListItem]
print(self.items.count)
}
@Published var name: String = ""
@Published var items: [MyListItem] = []
}
struct MyListsView: View {
@StateObject var vm: ViewModel
init(vm: ViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
let _ = Self._printChanges()
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
MyListCellView(vm: ListCellViewModel(myList: myList))
}
}
}
}
现在计数不再更新。
您的 ViewModel
是一个 ObserveableObject
,但您没有在 MyListsView
中观察到它。当你初始化 MyListsView
时,你设置了一个 let 常量。当然不会更新。改为这样做:
struct MyListsView: View {
@ObservedObject var vm: ViewModel
init(viewModel: ViewModel) {
self.vm = viewModel
}
var body: some View {
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
现在 ViewModel
中的 @Published
会导致 MyListView
发生变化,其中包括添加相关实体。
首先摆脱你的视图模型,我们不在 SwiftUI 中使用 MVVM,我们使用视图数据结构来处理我们的视图数据和 属性 包装器使它们表现得像对象。在您的情况下,对列表使用 @FetchRequest
属性 包装器,对细节使用 @ObservedObject
包装器,并且将在对模型数据进行任何更改时调用正文。检查 Xcode 中的应用程序模板中的代码,并检查核心数据。它看起来像这样:
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
DetailView(item: item)
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
...
struct DetailView: View {
@ObservedObject var item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")