在 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)")