A Core Data model with entity Node
having name
, createdAt
, to-many relationship children
and to-one relationship parent
(both optional). Using CodeGen Class Definition.
Using a @FetchRequest
with a predicate of parent == nil
, it's possible to grab the root nodes and subsequently walk the tree using the relationships.
Root nodes CRUD refreshes the view fine, but any modifications to child nodes don't display until restart although changes are saved in Core Data.
Simplest possible example in the code below illustrates the problem with child node deletion. The deletion works in Core Data but the view does not refresh if the deletion is on a child. The view refresh works fine if on a root node.
I'm new to Swift, so my apologies if this is a rather elementary question, but how can the view be refreshed upon changes to the child nodes?
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)
}
}
}
}
EDIT:
A trivial but unsatisfying solution is to make NodeListWakerCellView
retrieve the children using another @FetchRequest
but this feels wrong since the object is already available. Why run another query? But perhaps this is currently the only way to attach the publishing features?
I am wondering if there's another way to use a Combine
publisher directly to the children, perhaps within the .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)
}
}
}
}