In this code, I'm experiencing two issues while selecting items in a Picker:
This error occurs when selecting an item in the first Picker. The selection seems to be invalid, and I receive the above error for both parent and child items.
Search Field Issue: When I use the search field, all elements in the list disappear even though there should be matches. Could someone help me resolve these issues? Here’s the relevant code snippet:
import SwiftUI
struct Parent: Hashable {
let title: String
let children: [Child]
}
struct Child: Hashable {
let title: String
}
extension Parent {
static var mock: [Parent] = [
.init(title: "Parent 1", children: [.init(title: "Child 1"), .init(title: "Child 2")]),
.init(title: "Parent 2", children: [.init(title: "Child 3"), .init(title: "Child 4"), .init(title: "Child 5")])
]
}
@MainActor
@Observable
final class TestViewModel {
var parents: [Parent] = Parent.mock
var selectedParent: Parent
var searchParentText = ""
var searchParents: [Parent] {
if searchParentText.isEmpty {
return parents
} else {
return parents.filter { $0.title.contains(searchParentText) }
}
}
var children: [Child] {
selectedParent.children
}
var selectedChild: Child
var searchChildText = ""
var searchChildren: [Child] {
if searchChildText.isEmpty {
return children
} else {
return children.filter { $0.title.contains(searchChildText) }
}
}
init() {
self._selectedParent = .mock[0]
self._selectedChild = Parent.mock[0].children[0]
}
func updateSelectedItems() {
self.selectedChild = self.selectedParent.children[0]
}
}
@MainActor
struct TestSwiftUIView: View {
@State var vm = TestViewModel()
var body: some View {
NavigationStack {
Form {
Picker("Parents",
selection: $vm.selectedParent) {
ForEach(vm.searchParents, id: \.self) { parent in
Text(parent.title)
.tag(parent.title)
.searchable(text: $vm.searchParentText)
}
}
.pickerStyle(.navigationLink)
Picker("Parents",
selection: $vm.selectedChild) {
ForEach(vm.searchChildren, id: \.self) { child in
Text(child.title)
.tag(child.title)
.searchable(text: $vm.searchChildText)
}
}
.pickerStyle(.navigationLink)
}
.onChange(of: vm.selectedParent) {
vm.updateSelectedItems()
}
}
}
}
What could be causing these issues, and how can I fix them? Or there is any better way to achieve 2 pickers in relation parent/child, category/subcategory etc.
Firstly, you have a type mismatch. The first picker is bound to $vm.selectedParent
, which is of type Parent
. The tags have the value parent.title
, which is of type String
. The tags should have the same type as the bound variable. The same applies to the second picker.
As it happens, the way you currently have it might actually work, at least for the child, because the tag values are compared to the selectable items by their hash value. Still, I would suggest you change the tags to the following:
Text(parent.title)
.tag(parent)
//...
Text(child.title)
.tag(child)
This change won't fix the problem you reported. The reason it is happening is because the second picker contains the children of the selected parent. When you change the selection from Parent 1 to Parent 2, the second picker is automatically re-populated with the children of Parent 2. However, the previously-selected child belonged to Parent 1. This child is no longer found amongst the children of Parent 2, so you get the error.
When the parent selection changes, you are calling updateSelectedItems
to select the first child of the newly-selected parent. Unfortunately, this happens too late. The child picker is dynamically re-populated when the parent selection changes and when this happens it is not able to retain the previous child selection.
One way to fix would be to update the view model:
selectedParent
and update the new variable in willSet
and didSet
:// TestViewModel
private var previouslySelectedParent: Parent?
var selectedParent: Parent {
willSet {
if newValue != selectedParent {
previouslySelectedParent = selectedParent
}
}
didSet {
selectedChild = selectedParent.children[0]
previouslySelectedParent = nil
}
}
Then update the computed property that delivers the children:
var children: [Child] {
var combinedChildren = selectedParent.children
if let previouslySelectedParent, previouslySelectedParent != selectedParent {
combinedChildren += previouslySelectedParent.children
}
return combinedChildren
}
Finally, the didSet
function on selectedParent
is now performing the work of updateSelectedItems
, so you can remove this function and take out the .onChange
callback:
// func updateSelectedItems() {
// self.selectedChild = self.selectedParent.children[0]
// }
// TestSwiftUIView
Form {
// ...
}
// .onChange(of: vm.selectedParent) {
// vm.updateSelectedItems()
// }