0

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)
        }
    }
}
Benjohn
  • 13,228
  • 9
  • 65
  • 127
  • 1
    your code as is compiles and runs with me (Xcode 14b, iOS16 sim) – and does exactly what you wish :) – ChrisR Jul 03 '22 at 20:40
  • @ChrisR! Woah – that's potentially pretty epic :-) Thanks for the tip! I actually want it on Mac (initially at least), but if it works on upcoming iOS, perhaps it'll work on upcoming Mac too. – Benjohn Jul 03 '22 at 20:58
  • 1
    Try with OutlineGroup and/or DisclosureGroup to build hierarchy manually. – Asperi Jul 04 '22 at 05:00
  • @ChrisR Thanks – I added some updates about that. Also works back on iOS 15.5 with Xcode 13.4.1. But it doesn't work on Mac with either Xcode. Perhaps it'll need a Mac OS update! – Benjohn Jul 04 '22 at 10:45
  • @Asperi I will probably give that a try – thank you. – Benjohn Jul 04 '22 at 10:46
  • @Asperi I've tried an approach with `DisclosureGroup` similar to [the solution here](https://stackoverflow.com/questions/64236386/how-to-make-swiftui-list-outlinegroup-lazy-for-use-with-large-trees-like-a-file). Unfortunately it frequently doesn't display the content correctly on MacOS. If an open parent is closed then re-opened, it will, apparently randomly, re-open only some of its already open children. – Benjohn Jul 05 '22 at 12:35

0 Answers0