17

With List or OutlineGroup in SwiftUI how to make some (or all) of their branches expanded by default when creating the view. This seems to be possible with DisclosureGroup with a binding.

This could be useful for restoring state or customizing the view for presentation.

João Colaço
  • 1,202
  • 1
  • 14
  • 38

4 Answers4

7

Reusable version of OutlineGroup, where expandability is under control.

import SwiftUI

struct NodeOutlineGroup<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 {
        if node[keyPath: childKeyPath] != nil {
            DisclosureGroup(
                isExpanded: $isExpanded,
                content: {
                    if isExpanded {
                        ForEach(node[keyPath: childKeyPath]!) { childNode in
                            NodeOutlineGroup(node: childNode, childKeyPath: childKeyPath, isExpanded: isExpanded)
                        }
                    }
                },
                label: { Text(node.description) })
        } else {
            Text(node.description)
        }
    }
}

struct ContentView: View {
    var body: some View {
        List {
            NodeOutlineGroup(node: data, childKeyPath: \.children, isExpanded: true)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

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: "users", children:
    [FileItem(name: "user1234", children:
      [FileItem(name: "Photos", children:
        [FileItem(name: "photo001.jpg"),
         FileItem(name: "photo002.jpg")]),
       FileItem(name: "Movies", children:
         [FileItem(name: "movie001.mp4")]),
          FileItem(name: "Documents", children: [])
      ]),
     FileItem(name: "newuser", children:
       [FileItem(name: "Documents", children: [])
       ])
    ])

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Paul B
  • 3,989
  • 33
  • 46
4

Very nice, Paul B., thank you. But I would like to have more control about the displayed rows. So I have expanded your solution a little bit:

struct NodeOutlineGroup<Node, Content>: View where Node: Hashable, Node: Identifiable, Node: CustomStringConvertible, Content: View {
    let node: Node
    let childKeyPath: KeyPath<Node, [Node]?>
    @State var isExpanded: Bool = true
    let content: (Node) -> Content
    
    var body: some View {
        if node[keyPath: childKeyPath] != nil {
            DisclosureGroup(
                isExpanded: $isExpanded,
                content: {
                    if isExpanded {
                        ForEach(node[keyPath: childKeyPath]!) { childNode in
                            NodeOutlineGroup(node: childNode, childKeyPath: childKeyPath, isExpanded: isExpanded, content: content)
                        }
                    }
                },
                label: { content(node) })
        } else {
            content(node)
        }
    }
}

with usage:

struct ContentView: View {
    var body: some View {
        List {
            NodeOutlineGroup(node: data, childKeyPath: \.children, isExpanded: true) { node in
                Text(node.description)
            }
        }
    }
}
FPP
  • 278
  • 3
  • 7
3

I was searching for this as well and I believe OutlineGroup doesn't support this. Instead I've moved to DisclosureGroup, which OutlineGroup uses for it's implementation, and directly supports an expansion boolean binding as well as allowing nesting:

struct ToggleStates {
    var oneIsOn: Bool = false
    var twoIsOn: Bool = true
}
@State private var toggleStates = ToggleStates()
@State private var topExpanded: Bool = true

var body: some View {
    DisclosureGroup("Items", isExpanded: $topExpanded) {
        Toggle("Toggle 1", isOn: $toggleStates.oneIsOn)
        Toggle("Toggle 2", isOn: $toggleStates.twoIsOn)
        DisclosureGroup("Sub-items") {
            Text("Sub-item 1")
        }
    }
}

Example from https://developer.apple.com/documentation/swiftui/disclosuregroup

Evan Moran
  • 3,825
  • 34
  • 20
0

This is nice, but doesn’t allow for selection. Instead, the subtree under each first level item is composited as a single cell, which makes individual selection impossible.

Vince
  • 1
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Vikram Parimi Sep 16 '22 at 12:17