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

SwiftUI ScrollView停止滚动后选中值居中动画实现求助

问题解决:HorizontalPicker滚动停止后自动居中选中值

核心问题分析

当前代码通过监听每个item的位置变化来切换选中值,这种方式会在滚动过程中频繁触发选中状态变更,且滚动停止后没有强制将最终选中的值对齐到屏幕中心,导致出现选中值偏离中心的情况。

解决方案

我们需要在滚动停止后再触发选中值的判定与居中操作,而不是滚动过程中实时监听。以下是适配iOS 17+的优化实现(如果需要兼容更低版本,可通过UIViewRepresentable包装UIScrollView获取滚动停止事件):

import SwiftUI

struct HorizontalPicker<T: Hashable & CustomStringConvertible>: View {
    @Binding var selectedValue: T
    let values: [T]
    let itemWidth: CGFloat = 80
    
    // 存储每个item的全局位置
    @State private var itemFrames: [T: CGRect] = [:]
    
    var body: some View {
        VStack(alignment: .leading, spacing: 30) {
            VStack {
                Text("Select Value")
                    .fontWeight(.semibold)

                ZStack {
                    Circle()
                        .frame(width: 60)
                        .foregroundColor(.green)

                    ScrollView(.horizontal, showsIndicators: false) {
                        ScrollViewReader { scrollViewProxy in
                            HStack(spacing: 20) {
                                Spacer()
                                    .frame(width: (UIScreen.main.bounds.width / 2) - (itemWidth / 2))
                                
                                ForEach(values, id: \.self) { value in
                                    Text("\(value.description)")
                                        .font(.title2)
                                        .fontWeight(.bold)
                                        .padding()
                                        .foregroundColor(selectedValue == value ? .white : .black)
                                        .background(
                                            Circle()
                                                .frame(width: 40, height: 40)
                                                .foregroundColor(selectedValue == value ? .blue : .clear)
                                        )
                                        .background(GeometryReader { geo in
                                            Color.clear
                                                .onAppear {
                                                    // 记录item的初始位置
                                                    itemFrames[value] = geo.frame(in: .global)
                                                }
                                                .onChange(of: geo.frame(in: .global)) { newFrame in
                                                    // 更新item滚动时的位置
                                                    itemFrames[value] = newFrame
                                                }
                                        })
                                        .onTapGesture {
                                            withAnimation {
                                                selectedValue = value
                                                scrollViewProxy.scrollTo(value, anchor: .center)
                                            }
                                        }
                                        .id(value)
                                }
                                
                                Spacer()
                                    .frame(width: (UIScreen.main.bounds.width / 2) - (itemWidth / 2))
                            }
                            .onAppear {
                                DispatchQueue.main.async {
                                    scrollViewProxy.scrollTo(selectedValue, anchor: .center)
                                }
                            }
                        }
                    }
                    .frame(height: 100)
                    // iOS 17+ 滚动停止回调
                    .scrollEnded { direction in
                        guard let closestValue = findClosestValueToCenter() else { return }
                        withAnimation {
                            selectedValue = closestValue
                            // 强制滚动到中心
                            ScrollViewReader { proxy in
                                proxy.scrollTo(closestValue, anchor: .center)
                            }
                        }
                    }
                }
            }

            Spacer()
            
            VStack(alignment: .leading) {
                Text("Selected Value: \(selectedValue.description)")
            }
        }
        .padding()
    }
    
    // 找到最靠近屏幕中心的item
    private func findClosestValueToCenter() -> T? {
        let screenCenterX = UIScreen.main.bounds.width / 2
        var closestValue: T?
        var minDistance: CGFloat = .infinity
        
        for (value, frame) in itemFrames {
            let itemCenterX = frame.midX
            let distance = abs(itemCenterX - screenCenterX)
            if distance < minDistance {
                minDistance = distance
                closestValue = value
            }
        }
        
        return closestValue
    }
}

// Preview
struct HorizontalPickerPreview: View {
    @State private var selectedInt = 110
    @State private var selectedString = "Apple"
    @State private var selectedFloat: Float = 5.5

    var body: some View {
        VStack(spacing: 30) {
            HorizontalPicker(selectedValue: $selectedInt, values: Array(80...200))
                .frame(height: 150)

            HorizontalPicker(selectedValue: $selectedString, values: ["Apple", "Banana", "Cherry", "Date", "Elderberry"])
                .frame(height: 150)
            
            HorizontalPicker(selectedValue: $selectedFloat, values: stride(from: 1.0, through: 10.0, by: 0.5).map { Float($0) })
                .frame(height: 150)
        }
    }
}

#Preview {
    HorizontalPickerPreview()
}

关键改动说明

  1. 滚动停止触发逻辑:使用iOS 17新增的scrollEnded修饰符,仅在滚动完全停止后执行选中值判定与居中操作,避免滚动过程中的频繁切换。
  2. 位置记录与计算:通过GeometryReader记录每个item的全局位置,滚动停止时遍历所有item,计算出最靠近屏幕中心的那个值。
  3. 强制居中对齐:选中目标值后,通过ScrollViewReader强制将其滚动到屏幕中心,确保最终显示位置符合预期。
  4. 移除实时监听:删除原有的onChange(of: midX)逻辑,避免滚动过程中误触发选中状态。

如果需要兼容iOS 16及以下版本,可以通过UIViewRepresentable包装UIScrollView,利用UIScrollViewDelegate的scrollViewDidEndDecelerating和scrollViewDidEndDragging方法来监听滚动停止事件,核心逻辑与上述实现一致。

内容的提问来源于stack exchange,提问作者Abdul Rehman

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 20:22:13