I created a small sample app for saving and loading a tree using SwiftData, basically following this post. how-to-create-a-list-with-children-tree-from-swiftdata-model
The problem is, that it creates a doublet I don’t understand.
My model looks like this.
@Model
class ContentNode: Identifiable {
@Attribute(.unique) let id: UUID
let title: String
var children: [ContentNode]?
@Relationship(deleteRule: .cascade, inverse: \ContentNode.children) var parent: ContentNode?
init(title: String = "node") {
self.id = UUID()
self.title = title
}
}
My view consit of a simple List and I create nodes in a VM. I’m using recursion to find the parentNode and append a new node as child to it.
struct ContentView: View {
@State private var vm: ViewModel
init(modelContext: ModelContext, dbChanges: DbChanges) {
let viewModel = ViewModel(modelContext: modelContext)
_vm = State(wrappedValue: viewModel)
}
var body: some View {
NavigationStack {
List(vm.contentNodes, children: \.children, selection: $vm.selectedNodes) { node in
VStack(alignment: .leading) {
Text(node.title)
Text(node.id.uuidString)
.foregroundStyle(.tertiary)
}
.tag(node)
}
.toolbar {
Button("create") {
vm.create()
}
Button("child") {
vm.createChild()
}
}
}
.padding()
}
}
extension ContentView {
@Observable class ViewModel {
var modelContext: ModelContext
var contentNodes = [ContentNode]()
var selectedNodes = Set<ContentNode>()
init(modelContext: ModelContext) {
self.modelContext = modelContext
read()
}
func read() {
do {
let descriptor = FetchDescriptor<ContentNode>()
contentNodes = try modelContext.fetch(descriptor)
} catch {
print(error.localizedDescription)
}
}
func create() {
modelContext.insert(ContentNode())
read()
}
func createChild() {
if let selectedNode = selectedNodes.first {
if let parentNode = findNode().findParent(rootNodes: contentNodes, selectedNode: selectedNode) {
let node = ContentNode()
if let _ = parentNode.children {
parentNode.children?.append(node)
} else {
parentNode.children = []
parentNode.children?.append(node)
}
read()
}
}
}
}
}
The ModelContext I inject into ContentView is created like this. It’s all pretty standard.
@main
struct SwiftDataTreeSampleApp: App {
let container: ModelContainer
init() {
do {
self.container = try ModelContainer(for: ContentNode.self)
} catch {
print(error.localizedDescription)
fatalError()
}
}
var body: some Scene {
WindowGroup {
ContentView(modelContext: container.mainContext, dbChanges: dbChanges)
.modelContainer(container)
}
}
}
Your query in the read()
function returns all objects but it should only return the root/top level parents so add a predicate that checks if the parent property is nil
let descriptor = FetchDescriptor<ContentNode>(predicate: #Predicate {
$0.parent == nil
})