10

Here's a simple demo of the hierarchical List in SwiftUI. I'm testing it on macOS Big Sur, but unlike similar tree components in other UI toolkits, it asks for all its children immediately. So I can't use it for something like a file system browser.

Is there a way to make it lazy, so that it only asks for children when the UI element is expanded?

class Thing: Identifiable {
    let id: UUID
    let depth: Int
    let name: String
    init(_ name: String, depth: Int = 0) {
        self.id = UUID()
        self.name = name
        self.depth = depth
    }
    /// Lazy computed property
    var children: [Thing]? {
        if depth >= 5 { return nil }
        if _children == nil {
            print("Computing children property, name=\(name), depth=\(depth)")
            _children = (1...5).map { n in
                Thing("\(name).\(n)", depth:depth+1)
            }
        }
        return _children
    }
    private var _children: [Thing]? = nil
}

struct ContentView: View {
    var things: [Thing] = [Thing("1"), Thing("2"), Thing("3")]
    var body: some View {
        List(things, children: \.children) { thing in
            Text(thing.name)
        }
    }
}

Even though the initial UI only displays the top nodes:

You can see in the console that it asks for everything - all the way down the tree. This is a performance problem for large trees.

...
Computing children property, name=3.4.4.1.4, depth=4
Computing children property, name=3.4.4.1.5, depth=4
Computing children property, name=3.4.4.2, depth=3
Computing children property, name=3.4.4.2.1, depth=4
Computing children property, name=3.4.4.2.2, depth=4
...
Asperi
  • 228,894
  • 20
  • 464
  • 690
Rob N
  • 15,024
  • 17
  • 92
  • 165
  • 1
    Works fine with iOS 14, so seems this macOS issue. – Asperi Oct 07 '20 at 04:12
  • Others have observed this on macOS, as seen at https://developer.apple.com/forums/thread/662937 (FB8782243). It does render the OutlineGroup useless for any sort of deep or lazy hierarchy. – marcprux Dec 13 '20 at 20:48

1 Answers1

0

I believe this could be a bug in SwiftUI and I hope Apple will fix this. In the meantime, you can use the following workaround:

struct Node {
    var id: String
    var value: String
    var children: [Node]?
}

struct LazyDisclosureGroup: View {
    let node: Node
    @State var isExpanded: Bool = false

    var body: some View {
        if node.children != nil {
            DisclosureGroup(
                isExpanded: $isExpanded,
                content: {
                    if isExpanded {
                        ForEach(node.children!, id: \.self.id) { childNode in
                            LazyDisclosureGroup(node: childNode)
                        }
                    }
                },
                label: { Text(node.value) })
        } else {
            Text(node.value)
        }
    }
}

struct ContentView: View {
    let node = Node(
        id: "a",
        value: "a",
        children: [Node(id: "b", value: "b", children: nil),
                   Node(id: "c", value: "c", children: nil)])
    var body: some View {
        List {
            LazyDisclosureGroup(node: node)
        }
    }
}

I don't know if that's a best practice but the workaround uses the observation that DisclosureGroup "notices" when it is inside a List. The combination of the two produces the same visual structure and behaviour.

  • 1
    Hello Skimble! I was hopeful for your approach here and tried it out. I found that it seems to frequently not work very well. If I open the tree up, then close and open a high level node, many of the child nodes won't open again. Thinking the state might be getting destroyed when nodes are closed, I tried moving the `isExpanded` state to the Node as `@Published` and conformed `Node: ObservedObject` but this didn't resolve it. – Benjohn Jul 05 '22 at 12:29