SwiftUI: How to get selections from OutlineGroup?

2.9k views Asked by At

OutlineGroup is an analogue to NSOutlineView. NSOutlineView supports single/multiple node selection and we can obtain them by querying on NSOutlineView. Though obtaining selection on NSOutlineView is O(n), but this can be optimized to O(1) if the view tracks selection and provide them in proper interface.

How to obtain selections from OutlineGroup? Especially for multiple node selections. I checked out the manual entry, but couldn't find any mention about selection. What am I missing here?

4

There are 4 answers

0
Paul B On

Selectable List Here is the code.

import SwiftUI

struct ContentView: View {
    @State var selection = Set<FileItem.ID>()
    var body: some View {
        NavigationView {
            VStack {
                List(selection: $selection) {
                    OutlineGroup(data, children: \.children) { item in
                        Text("\(item.description)")
                    }
                    .onTapGesture {
                        print(selection)
                    }
                }
                .listStyle(InsetGroupedListStyle())
                .navigationTitle("Files")
                //.toolbar { EditButton() }
                .environment(\.editMode, .constant(.active))
                .onTapGesture {
                    // Walkaround: try how it works without `asyncAfter()`
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
                        print(selection)
                    })
                }
                Text("\(selection.count) selections")
            }
        }
        
    }
}

// Sample data:

struct FileItem: Hashable, Identifiable, CustomStringConvertible {
    var id: Self { self }
    var name: String
    var header: 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", header: "Header 1", children:
        [FileItem(name: "photo001.jpg", header: "Header 2"),
         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()
    }
}
1
Vince On

Using a NavigationLink as the view to represent the children of the tree actually do work

3
Asperi On

Documentation is not completely finished as it looks. Use direct SwiftUI autogenerated interfaces in Xcode 12 to find updates.

Especially for asked OutlineGroup there are several constructors with selection parameter, like below:

/// Creates a hierarchical list that computes its rows on demand from an
/// underlying collection of identifiable data, optionally allowing users to
/// select multiple rows.
///
/// - Parameters:
///   - data: The identifiable data for computing the list.
///   - selection: A binding to a set that identifies selected rows.
///   - rowContent: A view builder that creates the view for a single row of
///     the list.
@available(iOS 14.0, OSX 10.16, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public init<Data, RowContent>(_ data: Data, children: KeyPath<Data.Element, Data?>, 
    selection: Binding<Set<SelectionValue>>?, @ViewBuilder rowContent: @escaping (Data.Element) -> RowContent) where Content == OutlineGroup<Data, Data.Element.ID, HStack<RowContent>, HStack<RowContent>, DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>>, Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable
3
Ramis On

You need to put NavigationLink/Button for the item which do not have children.

Here is how it could look based on Apple source code.

var body: some View {
    OutlineGroup(data, children: \.children) { item in
        Group {
            if item.children == nil {
                NavigationLink(
                    destination: Text("\(item.name)"),
                    label: {
                        Text ("\(item.description)")
                    })
            } else {
                Text ("\(item.description)")
            }
        }
    }
}

The data comes from an Apple example. Sometimes links gets broken. So, here is source code:

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.count > 0 ? " \(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: [])
                            ])
                ])