Search code examples
swiftuirealm

Make Realm work with SwiftUI List(children:)


I am trying to use Realm as a database with a parent/children relationship and show the data in a hierarchical SwiftUI List using the children: initializer. I oriented myself at the SwiftUI+Realm tutorial. My Realm class looks like this:

import Foundation
import RealmSwift
    
class BlockList: RealmSwift.Object, RealmSwift.ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var id: RealmSwift.ObjectId
    @Persisted var title: String
    @Persisted var childBlockLists = RealmSwift.List<BlockList>()
    @Persisted(originProperty: "childBlockLists") var parentBlockList: RealmSwift.LinkingObjects<BlockList>
    
    convenience init(title: String) {
        self.init()
        self.title = title
   }
}

Then my content view looks like this:

struct ContentView: View {
    @ObservedResults(BlockList.self) var blockLists
    
    var body: some View {
        let parentBlockLists = blockLists.where {
            ($0.parentBlockList.count == 0)
        }
        
        List(parentBlockLists) { blockList in
            Text(blockList.title)
        }
        .toolbar {
            ToolbarItem {
                Button(action: addItem) {
                    Label("Add Item", systemImage: "plus")
                }
            }
        }
    }
    
    func addItem() {
        let realm = try! Realm()

        let newBlockList = BlockList(title: "New List")
        let newSubBlockList = BlockList(title: "New SubList")
        newBlockList.childBlockLists.append(newSubBlockList)
        try! realm.write {
            realm.add(newBlockList)
        }
    }
}

This shows all lists that do not have a parent. The next step would be to use List(children:) to show the data hierarchically, ie show those lists that have sublists with a chevron to expand the list.

For this purpose, the children parameter expects the key paths to the child nodes. However, I cannot figure out how to provide that. I tried to write an extension to BlockList and provide a function that casts the children into something useful, but none of my approaches work:

extension BlockList {
    var childBlockListsArray: AnyRealmCollection<BlockList> {
        AnyRealmCollection(childBlockLists)
//        guard let set = childBlockLists as? Array<BlockList>, set.isEmpty == false else { return nil }
//        childBlockLists.count == 0 ? nil : AnyRealmCollection(childBlockLists)
//        childBlockLists.count == 0 ? nil : Array(childBlockLists)
    }
}

I feel I get closest with casting in AnyRealmCollection and then use

List(AnyRealmCollection(parentBlockLists), children: \.childBlockListsArray)

But I still get the error

Key path value type 'AnyRealmCollection' cannot be converted to contextual type 'AnyRealmCollection?'

How can I provide the correct Realm data and their children key paths to be shown in SwiftUIs List?


Solution

  • Let me try an answer as I think that everything you need already exists in your question without any additional code.

    I believe the goal is to generate a list of parents, and then a chevron or disclosure triangles in the UI for the user to click on to then show their children - all done with one model.

    Here's your (simplified) model

    class BlockList: RealmSwift.Object {
        @Persisted var title: String
        @Persisted var childBlockLists = RealmSwift.List<BlockList>()
        @Persisted(originProperty: "childBlockLists") var parentBlockList: RealmSwift.LinkingObjects<BlockList>
    
        convenience init(title: String) {
            self.init()
            self.title = title
       }
    }
    

    Now populate realm with some data to test with; parents and children. In this case ParentA has two children while ParentB and ParentC have one.

    let parentA = BlockList(title: "parent A")
    let parentB = BlockList(title: "parent B")
    let parentC = BlockList(title: "parent C")
    
    let child0ofParentA = BlockList(title: "child 0 of Parent A")
    let child1ofParentA = BlockList(title: "child 1 of Parent A")
    
    let child0ofParentB = BlockList(title: "child 0 of Parent B")
    
    let child0ofParentC = BlockList(title: "child 0 of Parent C")
    
    parentA.childBlockLists.append(child0ofParentA)
    parentA.childBlockLists.append(child1ofParentA)
    
    parentB.childBlockLists.append(child0ofParentB)
    
    parentC.childBlockLists.append(child0ofParentC)
    
    try! realm.write {
        realm.add([parentA, parentB, parentC])
    }
    

    Then finally, let's read that data in and present a list - similar to what you'd do in the UI

    //get all of the parents
    let parents = realm.objects(BlockList.self).where { $0.childBlockLists.count > 0 }
    
    parents.forEach { parent in
        print("parent title: \(parent.title)")
    
        parent.childBlockLists.forEach { child in
            print("   child title: \(child.title)")
        }
    }
    

    and the output - which would be the same as a user taps a discosure triangle for each parent

    parent title: parent A
       child title: child 0 of Parent A
       child title: child 1 of Parent A
    parent title: parent B
       child title: child 0 of Parent B
    parent title: parent C
       child title: child 0 of Parent C
    

    Note that I am not using parentBlockList at all so it could be removed in this use case. Perhaps you want a want to transverse the graph back to the parent from the child? If so, leave it in.

    The SwiftUI part is just displaying a child objects just like the parent objects are displayed and you seems to know how to display a list of objects already.