I wish to use a picker with pickerStyle(.navigationLink) to select a value.
The list of values is long and so I would expect the picker list on the new screen to scroll automatically to ensure the currently selected value is visible. However on the new screen the first page is displayed regardless and you need to scroll manually to reach the selected value if it lies outside it.
Scrolling automatically occurs for pickerStyle(.menu) and pickerStyle(.wheel).
Is there a way of making pickerStyle(.navigationLink) reflect the same behaviour?
Demonstation code below:
import SwiftUI
struct ContentView: View {
@State private var selectedValue: Int = 56
let values = 0..<101
var body: some View {
NavigationStack{
Form {
Picker("Pick a value", selection:$selectedValue) {
ForEach(values, id:\.self) {
Text($0, format: .number)
}
}
.pickerStyle(.navigationLink)
}
}
}
}
You can create your own picker view that does this. Here is an example:
struct NavigationLinkPicker<Label: View, Content: View, Options: RandomAccessCollection>: View where Options.Element: Hashable {
typealias Selection = Options.Element
let label: Label
let options: Options
@Binding var selection: Selection
let content: (Selection) -> Content
init(options: Options, selection: Binding<Selection>, @ViewBuilder content: @escaping (Selection) -> Content, @ViewBuilder label: () -> Label) {
self.options = options
self._selection = selection
self.content = content
self.label = label()
}
var body: some View {
LabeledContent {
NavigationLink {
NavigationDestination(
selection: $selection,
options: options,
content: content
)
} label: {
HStack {
Spacer()
if let selectedOption = options.first(where: { $0 == selection }) {
content(selectedOption)
}
}
}
} label: {
label
}
}
struct NavigationDestination: View {
@Environment(\.dismiss) var dismiss
@Binding var selection: Selection
@State var listSelection: Selection?
let options: Options
let content: (Selection) -> Content
var body: some View {
ScrollViewReader { proxy in
List(options, id: \.self, selection: $listSelection) { option in
HStack {
content(option)
Spacer()
if option == selection {
Image(systemName: "checkmark")
.bold()
.foregroundStyle(Color.accentColor)
}
}
}
.onChange(of: listSelection) { _, newValue in
guard let newValue else { return }
Task {
// dismiss must be called with a little delay,
// or else the navigation destination is pushed again after dismissing
// for some reason
dismiss()
}
selection = newValue
}
.onAppear {
proxy.scrollTo(selection)
}
}
}
}
}
// Example usage:
struct ContentView: View {
@State private var selectedValue: Int = 56
let values = 0..<101
var body: some View {
NavigationStack{
Form {
NavigationLinkPicker(options: values, selection:$selectedValue) {
Text($0, format: .number)
} label: {
Text("Pick a value")
}
}
}
}
}
The above implementation requires you to pass in a RandomAccessCollection
to populate the picker with views. If you would like a usage more similar to the built-in picker, you can use this design instead, which depends on View Extractor.
import ViewExtractor
struct NavigationLinkPicker<Label: View, Content: View, Selection: Hashable>: View {
let label: Label
@Binding var selection: Selection
let content: Content
init(selection: Binding<Selection>, @ViewBuilder content: () -> Content, @ViewBuilder label: () -> Label) {
self._selection = selection
self.content = content()
self.label = label()
}
var body: some View {
Extract(content) { options in
LabeledContent {
NavigationLink {
NavigationDestination(selection: $selection, options: options)
} label: {
HStack {
Spacer()
options.first(where: { $0.id(as: Selection.self) == selection })
}
}
} label: {
label
}
}
}
struct NavigationDestination: View {
@Environment(\.dismiss) var dismiss
@Binding var selection: Selection
@State var listSelection: Selection?
let options: Views
var body: some View {
ScrollViewReader { proxy in
List(options, selection: $listSelection) { option in
if let id = option.id(as: Selection.self) {
HStack {
option
Spacer()
if id == selection {
Image(systemName: "checkmark")
.bold()
.foregroundStyle(Color.accentColor)
}
}
.tag(id)
.id(id)
}
}
.onChange(of: listSelection) { _, newValue in
guard let newValue else { return }
Task {
dismiss()
}
selection = newValue
}
.onAppear {
proxy.scrollTo(selection)
}
}
}
}
}
Note that in this design I am checking the id
s of the views, not tag
, so keep that in mind when using it. Reading tag
is something internal
to SwiftUI so we can't do that in our code.
Example usage:
struct ContentView: View {
@State private var selectedValue: Int = 56
let values = 0..<101
var body: some View {
NavigationStack{
Form {
NavigationLinkPicker(selection:$selectedValue) {
ForEach(values, id: \.self) {
Text($0, format: .number)
}
} label: {
Text("Pick a value")
}
}
}
}
}