You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI ScrollView导致WatchOS中Picker选择行为异常

问题解决:WatchOS ScrollView中Picker需双击选中的修复

问题现象

在WatchOS SwiftUI应用中,将多组Picker放入ScrollView后,首次点击目标Picker时会选中屏幕上的第一个Picker,必须再次点击才能选中目标;取消所有焦点后,该问题会重复出现。

问题根源

  1. 焦点绑定范围不当:原代码将FocusState绑定给包含多个Picker的HStack,而非单个Picker,导致点击时焦点会先落到整个HStack对应的第一个Picker上。
  2. 手势冲突: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 20:55:37