SwiftUI Multi-Segment Picker with dynamic data

I often have need for multi-segment (hierarchical) pickers with dynamic data: in UIKit I achieve this by reloading a subsidiary component after the user picks a value from the higher level component, e.g. for Country then State.


I've been trying to achieve the same sort of effect with a SwiftUI Form containing multiple (two to start with) Pickers, but without success. The ForEach(0 ..< elements.count) pattern doesn't work with second and subsequent levels in the hierarchy because the number of elements in these levels changes depending on the chosen top level, e.g. countries have different numbers of states. The compiler throws a warning that the ForEach(0 ..< ) pattern can only be used with constants and to use ForEach(array, id: ): the results are unpredictable anyway with the 0..< approach, with subscript out of range crashes.


I can get correct display of second level choices based on the selected first level, but the Selection binding of the Picker doesn't work, i.e. no check marks and no display of selected element - only the available choices:

Picker(selection: $selection, label: Text("SubGroup")) {

ForEach(subGroups, id: \.id) { subGroup in

Text(subGroup.element)

}

}


With the ForEach(0 ..< pattern, the $selection refers to an Int, whereas using an Identifiable array the selection is (presumably) based on the id. But how to specify the binding for an id-based selection? Indeed, is this possible at all? I've seen a few posts on the Web querying this, with no answer.


UPDATE: OK, fixed it 🙂 I'd been using UUID as the id for each group and each subgroup within a group. When I use a unique integer id (or double, e.g. 1.1, 1.2 etc for two-level hierarchy) in an identifiable model, everything is fine. However, my code needs to provide the id of the initial selected (first?) row of the lower-level Picker when the higher level has changed.


Regards,


Michaela

Replies

It took me a while to get my head around @ObservableObject, @Published, @ObservedObject and how to create a hierarchy of properties, so I'm providing my solution in the hope that it helps someone and/or someone suggests a better way of achieving the outcome. The aim is to display a form where the user selects a group, which then only shows the subgroup options for the selected group. This solution can be extended for further levels of the hierarchy, i.e. Sub-Subgroups: e.g. Country (Group). State (Subgroup), Locality (Sub-Subgroup).


struct Group : Identifiable {

var id = 0

var element: String

var subgroups: [Group]

}


class Selections : ObservableObject {


let groups = [Group(id: 10,element: "Group 1", subgroups: [Group(id: 11,element: "Subgroup 1.1",subgroups: []),Group(id: 12,element: "Subgroup 1.2",subgroups: []),Group(id: 13,element: "Subgroup 1.3",subgroups: [])]),Group(id: 20,element: "Group 2", subgroups: [Group(id:21,element: "Subgroup 2.1",subgroups: []),Group(id: 22,element: "Subgroup 2.2",subgroups: [])])]


@Published var selectedGroup : Int = 10 {

didSet {

selectedSubGroup = selectedGroup + 1

}

}


@Published var selectedSubGroup: Int = 11


func getSubGroups(group: Int) -> [Group] {

return groups.filter {$0.id == selectedGroup}.first!.subgroups

// or: return groups[(selectedGroup / 10) - 1].subgroups

}

}


class Data : ObservableObject {

static let `default` = Selections()

}



struct GroupPicker: View {

@ObservedObject var selections = Data.default

var body: some View {

Picker(selection: $selections.selectedGroup, label: Text("Group")) {

ForEach(selections.groups, id: \.id) { group in

Text(group.element)

}

}

}

}


struct SubGroupPicker: View {

@ObservedObject var selections = Data.default

var body: some View {

Picker(selection: $selections.selectedSubGroup, label: Text("SubGroup")) {

ForEach(selections.getSubGroups(group: selections.selectedGroup), id: \.id) { subGroup in

Text(subGroup.element)

}

}

}

}


struct ContentView: View {

@ObservedObject var selections = Selections()


var body: some View {

NavigationView {

Form {

Section {

GroupPicker()

SubGroupPicker()

}

}

}

}

}


************

Regards, Michaela

Hi Michaela,

Thanks for the code - it helped with my current project :-)

Just a question - I find that I have press a field down for about a second before the picker shows up.
Did you run into the same problem?

Any idea on how to fix that?
Don't worry about the previous post - I had a tapOnGesture that was causing the delay :-)