SwiftUI's List
has an init
accepting data elements along with a children: KeyPath
parameter to neatly support recursive data. Any element with children provides a disclosure indicator and can be opened to show its children.
Apple have an example to show use under "Creating hierarchical lists".
This is great and convenient. However, on Mac OS, List
seems to recursively visit all elements of the tree at the moment it is created. This unfortunately means it is not possible to use the feature for large or unterminated trees. Here is an example of use that SwiftUI will choke on:
import SwiftUI
final class Tree: Identifiable {
let id = UUID()
lazy var children: [Tree]? = {
[Tree(), Tree()]
}()
}
struct TreeView: View {
let tree = [Tree()]
var body: some View {
List(tree, children: \.children) { node in
Text(node.id.uuidString)
}
}
}
On iOS 15 this code will function and only the nodes that the user visits are created.
However, on Mac OS (Monterey), the code crashes on launch, making the feature unsuitable for unterminated recursive data.
- Is there an alternative built-in method that can be used with unterminated recursive data?
- Or is there some way use this feature to show unterminated recursive data? Perhaps nodes could lazily create any children as they are opened?
Thanks!
Updates…
As mentioned by ChrisR in comments, the example I've given runs perfectly with Xcode 14 – Beta 2 using an iOS 16 simulator. It also works with Xcode 13.4.1 with an iOS 15.5 simulator. The issue here appears to be with Mac OS (up to Monterey)
There is an extremely similar question also on SO. I've tried the answer given there, but the solution has display glitches, although it does prevent crashing.
Here's my version of that solution. The issue is that when a parent with disclosed children is closed then re-opened, often many of the open children are not shown, although the disclosure indicator shows that they should be.
final class Node: Identifiable, ObservableObject {
let id = UUID()
@Published var isExpanded = false
lazy var children: [Node] = {
[Node(), Node()]
}()
}
// MARK: -
struct TreeView: View {
let tree = Node()
var body: some View {
List {
TreeNode(node: tree)
}
}
}
private struct TreeNode: View {
@ObservedObject var node: Node
var body: some View {
DisclosureGroup(isExpanded: $node.isExpanded) {
if node.isExpanded, let children = node.children {
ForEach(children) {
TreeNode(node: $0)
}
}
} label: {
Text(node.id.uuidString)
}
}
}