I would like to show a directory structure with all nodes expanded in SwiftUI. I found an example of a recursive disclosure view structure on the internet.
The view:
struct TracksDirOutlineView<Node>: View where Node: Hashable, Node: Identifiable, Node: CustomStringConvertible{
let node: Node
let childKeyPath: KeyPath<Node, [Node]?>
@State var isExpanded: Bool = true
var body: some View {
GeometryReader { geometry in
if node[keyPath: childKeyPath] != nil {
DisclosureGroup(
isExpanded: $isExpanded,
content: {
if isExpanded {
ForEach(node[keyPath: childKeyPath]!) { childNode in
TracksDirOutlineView(node: childNode, childKeyPath: childKeyPath, isExpanded: isExpanded)
}
}
},
label: { Text(node.description) })
} else {
Text(node.description)
}
}
}
}
The model / view model:
struct FileItem: Hashable, Identifiable, CustomStringConvertible {
var id: Self { self }
var name: String
var children: [FileItem]? = nil
var description: String {
switch children {
case nil:
return "📄 \(name)"
case .some(let children):
return children.isEmpty ? "📂 \(name)" : "📁 \(name)"
}
}
}
let data =
FileItem(name: "root", children:
[FileItem(name: "child-1", children:
[FileItem(name: "child-1-1", children:
[FileItem(name: "child-1-1-1"),
FileItem(name: "child-1-1-2")]),
FileItem(name: "child-1-2", children:
[FileItem(name: "child-1-2-1")]),
FileItem(name: "child-1-3", children: [])
]),
FileItem(name: "child-2", children:
[FileItem(name: "child-2-1", children: [])
])
])
The call in ContentView:
TracksDirOutlineView(node: data, childKeyPath: \.children)
Somehow the view is cut off, even if there is still plenty of space in the view. I am not sure, if and where there should be a frame modifier. If I remove the GeometryReader, then the view becomes very large and everything is pushed to the bottom and partly off-screen
Also, is this the right approach? Should I use NSRepesentable with the NSOutlineView?
It looks like this could be fixed by just replacing the Geometry Reader
with VStack
and adding a Spacer()
at the bottom, e.g:
VStack {
if node[keyPath: childKeyPath] != nil {
DisclosureGroup(
isExpanded: $isExpanded,
content: {
if isExpanded {
ForEach(node[keyPath: childKeyPath]!) { childNode in
TracksDirOutlineView(node: childNode, childKeyPath: childKeyPath, isExpanded: isExpanded)
}
}
},
label: { Text(node.description) })
} else {
Text(node.description)
}
Spacer()
}