swiftdata

How to save a tree in SwiftData


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.

A child is created for the selected node but also another root level node.

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)
        }
    }
}

Solution

  • 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 
     })