SwiftUI @FetchRequest 对关系的核心数据更改不刷新

use*_*742 6 core-data swift swiftui combine

Node具有name, createdAt, 多对多关系children和一对一关系parent(均为可选)的实体的核心数据模型。使用 CodeGen 类定义。

使用@FetchRequest带有 谓词的 a parent == nil,可以获取根节点并随后使用关系遍历树。

根节点 CRUD 可以很好地刷新视图,但对子节点的任何修改在重新启动之前都不会显示,尽管更改已保存在 Core Data 中。

下面代码中最简单的示例说明了子节点删除的问题。删除在 Core Data 中有效,但如果删除是在子级上,则视图不会刷新。如果在根节点上,视图刷新工作正常。

我是 Swift 的新手,所以如果这是一个相当基本的问题,我很抱歉,但是如何在更改子节点时刷新视图?

import SwiftUI
import CoreData

extension Node {

    class func count() -> Int {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        let fetchRequest: NSFetchRequest<Node> = Node.fetchRequest()

        do {
            let count = try context.count(for: fetchRequest)
            print("found nodes: \(count)")
            return count
        } catch let error as NSError {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    }
}

struct ContentView: View {

    @Environment(\.managedObjectContext) var managedObjectContext

    @FetchRequest(entity: Node.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: true)], predicate: NSPredicate(format: "parent == nil"))
    var nodes: FetchedResults<Node>

    var body: some View {

        NavigationView {
            List {
                NodeWalkerView(nodes: Array(nodes.map { $0 as Node })  )
            }
            .navigationBarItems(trailing: EditButton())
        }
        .onAppear(perform: { self.loadData() } )

    }
    func loadData() {
        if Node.count() == 0 {
            for i in 0...3 {
                let node = Node(context: self.managedObjectContext)
                node.name = "Node \(i)"
                for j in 0...2 {
                    let child = Node(context: self.managedObjectContext)
                    child.name = "Child \(i).\(j)"
                    node.addToChildren(child)
                    for k in 0...2 {
                        let subchild = Node(context: self.managedObjectContext)
                        subchild.name = "Subchild \(i).\(j).\(k)"
                        child.addToChildren(subchild)
                    }
                }
            }
            do {
                try self.managedObjectContext.save()
            } catch {
                print(error)
            }
        }
    }
}

struct NodeWalkerView: View {

    @Environment(\.managedObjectContext) var managedObjectContext

    var nodes: [Node]

    var body: some View {

        ForEach( self.nodes, id: \.self ) { node in
            NodeListWalkerCellView(node: node)
        }
        .onDelete { (indexSet) in
            let nodeToDelete = self.nodes[indexSet.first!]
            self.managedObjectContext.delete(nodeToDelete)
            do {
                try self.managedObjectContext.save()
            } catch {
                print(error)
            }
        }
    }
}

struct NodeListWalkerCellView: View {

    @ObservedObject var node: Node

    var body: some View {

        Section {
            Text("\(node.name ?? "")")
            if node.children!.count > 0 {
                NodeWalkerView(nodes: node.children?.allObjects as! [Node] )
                .padding(.leading, 30)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑:

一个微不足道但不令人满意的解决方案是NodeListWakerCellView使用另一个来检索孩子,@FetchRequest但这感觉不对,因为对象已经可用。为什么要运行另一个查询?但也许这是目前附加发布功能的唯一方法?

我想知道是否有另一种方法可以Combine直接向孩子们使用出版商,也许在.map?

struct NodeListWalkerCellView: View {

    @ObservedObject var node: Node

    @FetchRequest var children: FetchedResults<Node>

    init( node: Node ) {
        self.node = node
        self._children = FetchRequest(
            entity: Node.entity(),
            sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: false)],
            predicate: NSPredicate(format: "%K == %@", #keyPath(Node.parent), node)
        )
    }

    var body: some View {

        Section {
            Text("\(node.name ?? "")")
            if node.children!.count > 0 {

                NodeWalkerView(nodes: children.map({ $0 as Node }) )

                .padding(.leading, 30)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

lor*_*sum 3

NSManagedObjectContextObjectsDidChange Notification通过观察和刷新,您可以轻松观察到所有变化View

在下面的代码中,您可以按如下方式复制该问题。

  1. 创建Node具有以下属性和关系的实体 在此输入图像描述

2.将以下代码粘贴到项目中

  1. 运行NodeContentView模拟器

  2. 在第一个屏幕中选择一个节点

  3. 编辑节点名称

  4. 单击“返回”按钮

  5. 请注意,所选变量的名称没有更改。

怎么解决”

  1. 取消注释//NotificationCenter.default.addObserver(self, selector: #selector(refreshView), name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)位于initCoreDataPersistence

  2. 按照步骤 3-6 操作

  3. 请注意,这次更新了节点名称。

import SwiftUI
import CoreData

extension Node {
    public override func awakeFromInsert() {
        super.awakeFromInsert()
        self.createdAt = Date()
    }
}
///Notice the superclass the code is below
class NodePersistence: CoreDataPersistence{
    func loadSampleData() {
        if NodeCount() == 0 {
            for i in 0...3 {
                let node: Node = create()
                node.name = "Node \(i)"
                for j in 0...2 {
                    let child: Node = create()
                    child.name = "Child \(i).\(j)"
                    node.addToChildren(child)
                    for k in 0...2 {
                        let subchild: Node = create()
                        subchild.name = "Subchild \(i).\(j).\(k)"
                        child.addToChildren(subchild)
                    }
                }
            }
            save()
        }
    }
    func NodeCount() -> Int {
        let fetchRequest: NSFetchRequest<Node> = Node.fetchRequest()
        
        do {
            let count = try context.count(for: fetchRequest)
            print("found nodes: \(count)")
            return count
        } catch let error as NSError {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    }
}

struct NodeContentView: View {
    //Create the class that sets the appropriate context
    @StateObject var nodePers: NodePersistence = .init()
    
    var body: some View{
        NodeListView()
        //Pass the modified context
            .environment(\.managedObjectContext, nodePers.context)
            .environmentObject(nodePers)
    }
}

struct NodeListView: View {
    @EnvironmentObject var nodePers: NodePersistence
    @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: true)], predicate: NSPredicate(format: "parent == nil"))
    var nodes: FetchedResults<Node>
    
    var body: some View {
        NavigationView {
            List {
                NodeWalkerView(nodes: Array(nodes))
            }
            .navigationBarItems(trailing: EditButton())
            .navigationTitle("select a node")
        }
        .onAppear(perform: { nodePers.loadSampleData()} )
    }
}

struct NodeWalkerView: View {
    @EnvironmentObject var nodePers: NodePersistence
    //This breaks observation, it has no SwiftUI wrapper
    var nodes: [Node]
    
    var body: some View {
        Text(nodes.count.description)
        ForEach(nodes, id: \.objectID ) { node in
            NavigationLink(node.name.bound, destination: {
                NodeListWalkerCellView(node: node)
            })
        }
        .onDelete { (indexSet) in
            for idx in indexSet{
                nodePers.delete(nodes[idx])
            }
        }
    }
}

struct NodeListWalkerCellView: View {
    @EnvironmentObject var nodePers: NodePersistence
    
    @ObservedObject var node: Node
    
    var body: some View {
        Section {
            //added
            TextField("name",text: $node.name.bound) //<---Edit HERE
                .textFieldStyle(.roundedBorder)
            if node.children?.allObjects.count ?? -1 > 0{
                NavigationLink(node.name.bound, destination: {
                    NodeWalkerView(nodes: node.children?.allObjects.typeArray() ?? [])
                        .padding(.leading, 30)
                })
            }else{
                Text("empty has no children")
            }
            
        }.navigationTitle("Edit name on this screen")
    }
}

extension Array where Element: Any{
    func typeArray<T: Any>() -> [T]{
        self as? [T] ?? []
    }
}
struct NodeContentView_Previews: PreviewProvider {
    static var previews: some View {
        NodeContentView()
    }
}
extension Optional where Wrapped == String {
    var _bound: String? {
        get {
            return self
        }
        set {
            self = newValue
        }
    }
    var bound: String {
        get {
            return _bound ?? ""
        }
        set {
            _bound = newValue
        }
    }
    
}
///Generic CoreData Helper not needed just to make stuff easy.
class CoreDataPersistence: ObservableObject{
    //Use preview context in canvas/preview
    //The setup for this is in XCode when you create a new project
    let context = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" ? PersistenceController.preview.container.viewContext : PersistenceController.shared.container.viewContext
    
    init(){
        //Observe all the changes in the context, then refresh the View that observes this using @StateObject, @ObservedObject or @EnvironmentObject
        //There are other options, like NSPersistentStoreCoordinatorStoresDidChange for the coordinator
        //https://developer.apple.com/documentation/foundation/nsnotification/name/1506884-nsmanagedobjectcontextobjectsdid
        //NotificationCenter.default.addObserver(self, selector: #selector(refreshView), name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
    }
    ///Creates an NSManagedObject of any type
    func create<T: NSManagedObject>() -> T{
        T(context: context)
        //Can set any defaults in awakeFromInsert() in an extension for the Entity
        //or override this method using the specific type
    }
    ///Updates an NSManagedObject of any type
    func update<T: NSManagedObject>(_ obj: T){
        //Make any changes like a last modified variable
        save()
    }
    ///Creates a sample
    func addSample<T: NSManagedObject>() -> T{
        
        return create()
    }
    ///Deletes  an NSManagedObject of any type
    func delete(_ obj: NSManagedObject){
        context.delete(obj)
        save()
    }
    func resetStore(){
        context.rollback()
        save()
    }
    internal func save(){
        do{
            try context.save()
        }catch{
            print(error)
        }
    }
    @objc
    func refreshView(){
        objectWillChange.send()
    }
}
Run Code Online (Sandbox Code Playgroud)

CoreDataPersistenceclass是可以与任何实体一起使用的泛型。只需将其复制到您的项目中,您就可以将其用作superclass您自己的 CoreData ViewModel,或者如果您没有任何要覆盖或添加的内容,则按原样使用它。

该解决方案的关键部分是未注释的行以及selector告诉View重新加载的行。其他一切都是额外的

代码看起来很多,因为这是OP提供的,但解决方案包含在CoreDataPersistence. 请注意 theNodeContentViewcontext应该@FetchRequest与 the 相匹配CoreDataPersistence

选项2

对于这个特定的用例(子级与父级具有相同的类型),您可以使用Listwith childreninit它简化了很多设置,并且大大减少了更新问题。

extension Node {
    public override func awakeFromInsert() {
        super.awakeFromInsert()
        self.createdAt = Date()
    }
    @objc
    var typedChildren: [Node]?{
        self.children?.allObjects.typeArray()
    }
}
struct NodeListView: View {
    @EnvironmentObject var nodePers: NodePersistence
    @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: true)], predicate: NSPredicate(format: "parent == nil"))
    var nodes: FetchedResults<Node>
    
    var body: some View {
        NavigationView {
            List(Array(nodes) as [Node], children: \.typedChildren){node in
                NodeListWalkerCellView(node: node)
            }
            
            .navigationBarItems(trailing: EditButton())
            .navigationTitle("select a node")
        }
        .onAppear(perform: { nodePers.loadSampleData()} )
    }
}

struct NodeListWalkerCellView: View {
    @EnvironmentObject var nodePers: NodePersistence
    @ObservedObject var node: Node
    
    var body: some View {
        HStack {
            //added
            TextField("name",text: $node.name.bound)
                .textFieldStyle(.roundedBorder)
            Button("delete", role: .destructive, action: {
                nodePers.delete(node)
            })
            
        }.navigationTitle("Edit name on this screen")
    }
}
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

1508 次

最近记录:

5 年,4 月 前