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
?
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.