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

自定义UIScrollView选择器:滚动时修改选中行字体与颜色

问题解决:自定义滚动选择器的选中项样式动态修改

核心问题分析

  1. 原代码中两个滑块共用同一个offset绑定变量,导致滚动时互相干扰,无法独立判断各自的选中项
  2. 所有文本样式固定,未根据滚动偏移动态调整选中项的字体和颜色
  3. 滚动结束后的位置计算逻辑有误,无法保证选中项居中显示

修改后的完整代码

CustomPicker 主视图

struct CustomPicker: View {
    // 为两个滑块设置独立的偏移量,初始值让第一个选中项居中
    @State var integerOffset: CGFloat = 90
    @State var doubleOffset: CGFloat = 90
    
    let integers: [Int]
    let doubles: [String]
    
    // 计算当前选中的整数索引(限制在有效范围内)
    private var selectedIntegerIndex: Int {
        let index = (integerOffset / 90).rounded(.toNearestOrAwayFromZero)
        return Int(max(0, min(Double(integers.count - 1), index)))
    }
    
    // 计算当前选中的小数索引(限制在有效范围内)
    private var selectedDoubleIndex: Int {
        let index = (doubleOffset / 90).rounded(.toNearestOrAwayFromZero)
        return Int(max(0, min(Double(doubles.count - 1), index)))
    }
    
    var body: some View {
        VStack {
            HStack(spacing: 0) {
                CustomSlider(sliderItems: integers.count, offset: $integerOffset) {
                    VStack(spacing: 0) {
                        // 通过enumerated获取每个item的索引
                        ForEach(integers.enumerated(), id: \.element) { index, num in
                            VStack {
                                Text("\(num)")
                                    // 根据是否为选中项动态调整样式
                                    .font(.custom("Calibre", size: index == selectedIntegerIndex ? 65 : 45))
                                    .foregroundColor(index == selectedIntegerIndex ? .gray : .black)
                                    .frame(maxWidth: 90, maxHeight: 90)
                            }
                            .onTapGesture {
                                // 点击直接滚动到对应位置
                                withAnimation {
                                    integerOffset = CGFloat(index) * 90
                                }
                            }
                        }
                    }
                }
                .frame(maxWidth: 90, maxHeight: 270)
                
                Text(",")
                    .font(.custom("Calibre", size: 65))
                
                CustomSlider(sliderItems: doubles.count, offset: $doubleOffset) {
                    VStack(spacing: 0) {
                        ForEach(doubles.enumerated(), id: \.element) { index, str in
                            VStack {
                                Text(str)
                                    .font(.custom("Calibre", size: index == selectedDoubleIndex ? 65 : 45))
                                    .foregroundColor(index == selectedDoubleIndex ? .gray : .black)
                                    .frame(maxWidth: 90, maxHeight: 90)
                            }
                            .onTapGesture {
                                withAnimation {
                                    doubleOffset = CGFloat(index) * 90
                                }
                            }
                        }
                    }
                }
                .frame(maxWidth: 90, maxHeight: 270)
            }
        }
    }
}

CustomSlider 自定义滚动组件

struct CustomSlider<Content: View>: UIViewRepresentable {
    let content: Content
    @Binding var offset: CGFloat
    let sliderItems: Int
    
    init(sliderItems: Int, offset: Binding<CGFloat>, @ViewBuilder content: @escaping () -> Content) {
        self.content = content()
        self._offset = offset
        self.sliderItems = sliderItems
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    func makeUIView(context: Context) -> UIScrollView {
        let scrollView = UIScrollView()
        let swiftUIView = UIHostingController(rootView: content).view!
        
        // 内容高度:所有item高度 + 上下各一个item的高度,确保滚动时能看到前后项
        let height = CGFloat(sliderItems * 90) + 180
        swiftUIView.frame = CGRect(x: 0, y: 0, width: 90, height: height)
        
        scrollView.contentSize = swiftUIView.frame.size
        scrollView.addSubview(swiftUIView)
        scrollView.bounces = false
        scrollView.showsVerticalScrollIndicator = false
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.delegate = context.coordinator
        scrollView.isDirectionalLockEnabled = true
        
        // 初始偏移设置为让第一个选中项居中
        scrollView.contentOffset.y = offset
        
        return scrollView
    }
    
    func updateUIView(_ uiView: UIScrollView, context: Context) {
        // 同步绑定偏移量与scrollView实际偏移
        if uiView.contentOffset.y != offset {
            uiView.setContentOffset(CGPoint(x: 0, y: offset), animated: false)
        }
    }
    
    class Coordinator: NSObject, UIScrollViewDelegate {
        var parent: CustomSlider
        
        init(parent: CustomSlider) {
            self.parent = parent
        }
        
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            parent.offset = scrollView.contentOffset.y
        }
        
        func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
            adjustScrollPosition(scrollView: scrollView)
        }
        
        func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
            if !decelerate {
                adjustScrollPosition(scrollView: scrollView)
            }
        }
        
        // 调整滚动位置,确保选中项居中显示
        private func adjustScrollPosition(scrollView: UIScrollView) {
            let visibleCenterY = scrollView.contentOffset.y + scrollView.bounds.height / 2
            let index = round((visibleCenterY - 45) / 90)
            let clampedIndex = max(0, min(Double(parent.sliderItems - 1), index))
            let targetOffsetY = clampedIndex * 90 + 45 - scrollView.bounds.height / 2
            scrollView.setContentOffset(CGPoint(x: 0, y: targetOffsetY), animated: true)
        }
    }
}

// 辅助函数
func getRect() -> CGRect {
    return UIScreen.main.bounds
}

关键修改点说明

  • 独立偏移控制:为两个滑块分别设置integerOffset和doubleOffset,避免滚动干扰
  • 动态样式切换:通过enumerated()获取item索引,结合选中索引判断,动态修改字体大小和颜色
  • 精准居中逻辑:修复滚动结束后的位置计算,确保选中项始终处于滑块中间位置
  • 点击滚动支持:添加item点击事件,直接滚动到对应位置并触发样式更新

内容的提问来源于stack exchange,提问作者Alex Anghel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 10:04:54