I’ve promised to present an improved version of the OutlinePicker
shown in this post and here it is. What did I do?
- The parameter list changed a little bit. Now there’s a special title for „No Selection“ and we do not need the
isExpanded
parameter anymore since we want to expand the nodes automatically according to the selection. - I’ve introduced the new array
nodesToExpand
. I’ll come back to that a little bit later. - Instead of a sheet the selection view is presented as a popover (
WithPopover
from this post). Using that we do not need a close button anymore. - The NodeOutlineGroup from this post is used to accept the array of expanded nodes.
- Instead of a
Form
I use aList
here with a plainlistStyle
. That looks better in the popover view. - The „No Selection“ section is now separated from the list in a similar manner as a
Divider
would do it in aPicker
view.
Here is the coding:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
struct OutlinePicker<Node, Content>: View where Node: Hashable, Node: Identifiable, Content: View { let title: String // the title of the picker let noSelection: String // and the string for no selection let nodes: [Node] // and the array of nodes to display let childKeyPath: KeyPath<Node, [Node]?> // and the key path to its children @Binding var selection: Node? // the selected node let content: (Node) -> Content // and what to display on each node @State private var showSheet = false // do we show the sheet? private let checkMarkWidth: CGFloat = 27 // the width of the checkmark symbol private var nodesToExpand: [Node] { // the array of nodes which should be expanded on startup let nodesExpand = NodesToExpand(nodes: nodes, selection: selection, childKeyPath: childKeyPath) return nodesExpand.checkNodes() // let the class calculate that } var body: some View { HStack { // make it similar to the standard picker Text(title) // display the title Spacer() // and the rest right aligned WithPopover(showPopover: $showSheet, content: { Button(action: { // the trigger for the popover showSheet = true // show the sheet }, label: { HStack { // display the current selection if let selected = selection { // we actually do have a selection content(selected) // display its content } else { // no, there is no selection Text(noSelection) // signal that } Image(systemName: "chevron.up.chevron.down") // indicate the picker .font(.footnote) // with the same size as the standard picker } }) .foregroundStyle(.secondary) // in gray }, popoverContent: { List { // show the outline in a list ForEach(nodes) { node in // for every node show the outline group NodeOutlineGroup(node: node, childKeyPath: childKeyPath, isExpanded: false, expandedNodes: nodesToExpand, content: { n in Button(action: { // and make the content selectable selection = n // remember the selected node showSheet = false // and stop the sheet }, label: { HStack { // what do we want to display at each node if n == selection { // this node is the currently selected one Image(systemName: "checkmark") // signal that } else { // no, its not the currently selected one Spacer() // leave some room .frame(width: checkMarkWidth) // to make the columns left aligned } content(n) // and show the content of the node Spacer() // left aligned } }) .foregroundStyle(.primary) // in standard color }) } Divider() // some separator .frame(width: 1000 , height: 8) // with full width and some height .overlay(Color(UIColor.systemGray5)) // in gray Button(action: { // and add the option for no selection selection = nil // signal that showSheet = false // and stop the sheet as well }, label: { // display that quite similar HStack { if selection == nil { // currently there is no selection Image(systemName: "checkmark") // signal that } else { // no, we do have a selection currenlty Spacer() // leave some room for the checkmark .frame(width: checkMarkWidth) // to make the columns left aligned } Text(noSelection) // the text of no selection Spacer() // left aligned } }) .foregroundStyle(.primary) // in standard color } .listStyle(.plain) // just a plain list .environment(\.defaultMinListRowHeight, 8) }) } } // MARK: - NodesToExpand private class NodesToExpand { let nodes: [Node] // and the array of nodes to display let selection: Node? // the selected node let childKeyPath: KeyPath<Node, [Node]?> // and the key path to its children private var nodesToExpand = [Node]() // the array of expanded nodes /// Intialize this class /// - Parameters: /// - nodes: the `[Node]` to display /// - selection: the currenlty selected `Node?` /// - childKeyPath: the `KeyPath` to the node children /// - Ver: 1.0 03.08.2023 fpp init(nodes: [Node], selection: Node?, childKeyPath: KeyPath<Node, [Node]?>) { self.nodes = nodes // set the properties self.selection = selection self.childKeyPath = childKeyPath } /// Recursively check for the selected node /// - Parameters: /// - theNode: the `Node` to check /// - Returns: /// `Bool`: selected node found /// - Ver: 1.0 03.08.2023 fpp private func checkNode(theNode node: Node) -> Bool { if node == selection { // we did find the selected node return true // signal that up the tree } else { // no, not found yet if let children = node[keyPath: childKeyPath] { // look in the sub tree for child in children { // loop over all children if checkNode(theNode: child) { // and call us recursively nodesToExpand.append(node) // yes, found in subtree, remember the parent return true // and signal success } } } } return false // no, not found in tree } /// Get the nodes to be expanded /// - Returns: /// `[Node]`: the array of expanded nodes /// - Ver: 1.0 03.08.2023 fpp func checkNodes() -> [Node] { nodesToExpand.removeAll() // clear the array to return if selection != nil { // something to look for for node in nodes { // loop over all nodes if checkNode(theNode: node) { // and check for the selection break // no need to continue } } } return nodesToExpand // and return the array } } } |
So what about the nodesToExpand
array? This was my main motivation to develop this improved version of OutlinePicker
. If the currently selected node is deep in the tree of nodes it would be nice if the tree is expanded on the path to this node and only there.
For that we have to traverse through the tree of nodes until we find the selected one and have to remember the parent nodes up to that point. I’ve achieved this by building up the array of nodes to expand recursively with the method checkNode
. It returns true if the selected one is found in the current node or in one of its children and is building up the array of parents as a side effect.
All this functionality is implemented in the internal class NodesToExpand
.
Here you can see how this new version of OutlinePicker looks like in the same example as in this previous post. Have fun.