SwiftUI ScrollView导致WatchOS中Picker选择行为异常
问题解决:WatchOS ScrollView中Picker需双击选中的修复
问题现象
在WatchOS SwiftUI应用中,将多组Picker放入ScrollView后,首次点击目标Picker时会选中屏幕上的第一个Picker,必须再次点击才能选中目标;取消所有焦点后,该问题会重复出现。
问题根源
- 焦点绑定范围不当:原代码将
FocusState绑定给包含多个Picker的HStack,而非单个Picker,导致点击时焦点会先落到整个HStack对应的第一个Picker上。 - 手势冲突:WatchOS的ScrollView滚动手势与Picker的点击手势存在优先级冲突,首次点击会被ScrollView的手势处理拦截,无法直接激活目标Picker。
修复后的代码
import SwiftUI struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } struct ContentView: View { let paceArray = Array(0...59) let speedArray = Array(0...99) @State private var globalSecondsPerKM: Double = 0 @State private var paceMKMHours: Int = 0 @State private var paceMKMMinutes: Int = 0 @State private var paceMKMSeconds: Int = 0 @State private var paceMMHours: Int = 0 @State private var paceMMMinutes: Int = 0 @State private var paceMMSeconds: Int = 0 @State private var speedKMHWhole: Int = 0 @State private var speedKMHDecimal: Int = 0 // 为每个单独的Picker创建FocusState @FocusState private var paceMKMHoursFocused: Bool @FocusState private var paceMKMMinutesFocused: Bool @FocusState private var paceMKMSecondsFocused: Bool @FocusState private var paceMMHoursFocused: Bool @FocusState private var paceMMMinutesFocused: Bool @FocusState private var paceMMSecondsFocused: Bool @FocusState private var speedKMHWholeFocused: Bool @FocusState private var speedKMHDecimalFocused: Bool var body: some View { ScrollView { VStack { VStack { Text("Pace per km") .font(.headline) HStack { Picker(selection: $paceMKMHours, label: Text("")) { ForEach(0..<speedArray.count, id: \.self) { index in Text(String(format: "%02dh", speedArray[index])).tag(index) } } .frame(width: 45) .focused($paceMKMHoursFocused) .highPriorityGesture(TapGesture().onEnded { paceMKMHoursFocused = true }) Picker(selection: $paceMKMMinutes, label: Text("")) { ForEach(0..<paceArray.count, id: \.self) { index in Text(String(format: "%02dm", paceArray[index])).tag(index) } } .frame(width: 45) .focused($paceMKMMinutesFocused) .highPriorityGesture(TapGesture().onEnded { paceMKMMinutesFocused = true }) Picker(selection: $paceMKMSeconds, label: Text("")) { ForEach(0..<paceArray.count, id: \.self) { index in Text(String(format: "%02ds", paceArray[index])).tag(index) } } .frame(width: 45) .focused($paceMKMSecondsFocused) .highPriorityGesture(TapGesture().onEnded { paceMKMSecondsFocused = true }) } .padding(.bottom, 5) .frame(height: 35) } Divider() VStack { Text("Pace per mile") .font(.headline) HStack { Picker(selection: $paceMMHours, label: Text("")) { ForEach(0..<speedArray.count, id: \.self) { index in Text(String(format: "%02dh", speedArray[index])).tag(index) } } .frame(width: 45) .focused($paceMMHoursFocused) .highPriorityGesture(TapGesture().onEnded { paceMMHoursFocused = true }) Picker(selection: $paceMMMinutes, label: Text("")) { ForEach(0..<paceArray.count, id: \.self) { index in Text(String(format: "%02dm", paceArray[index])).tag(index) } } .frame(width: 45) .focused($paceMMMinutesFocused) .highPriorityGesture(TapGesture().onEnded { paceMMMinutesFocused = true }) Picker(selection: $paceMMSeconds, label: Text("")) { ForEach(0..<paceArray.count, id: \.self) { index in Text(String(format: "%02ds", paceArray[index])).tag(index) } } .frame(width: 45) .focused($paceMMSecondsFocused) .highPriorityGesture(TapGesture().onEnded { paceMMSecondsFocused = true }) } .padding(.bottom, 5) .frame(height: 35) } Divider() VStack { Text("Speed in km/h") .font(.headline) HStack { Picker(selection: $speedKMHWhole, label: Text("")) { ForEach(0..<speedArray.count, id: \.self) { index in Text(String(format: "%02d", speedArray[index])).tag(index) } } .frame(width: 45) .focused($speedKMHWholeFocused) .highPriorityGesture(TapGesture().onEnded { speedKMHWholeFocused = true }) Picker(selection: $speedKMHDecimal, label: Text("")) { ForEach(0..<speedArray.count, id: \.self) { index in Text(String(format: ".%02d", speedArray[index])).tag(index) } } .frame(width: 45) .focused($speedKMHDecimalFocused) .highPriorityGesture(TapGesture().onEnded { speedKMHDecimalFocused = true }) } .padding(.bottom, 5) .frame(height: 35) } Divider() } } .labelsHidden() .font(.system(size: 13)) } }
关键修改点
- 拆分FocusState:将原来针对每组的
FocusState拆分为每个Picker单独的状态,确保点击时能精准激活目标Picker的焦点。 - 添加高优先级手势:通过
.highPriorityGesture(TapGesture())强制让Picker的点击手势优先级高于ScrollView的滚动手势,避免首次点击被滚动手势拦截。 - 单独绑定焦点:每个Picker都单独绑定对应的
FocusState,替换原有的整组绑定,消除焦点误触到其他Picker的问题。
内容的提问来源于stack exchange,提问作者kattsmurf
相关产品推荐
相关产品推荐

