自定义UIScrollView选择器:滚动时修改选中行字体与颜色
问题解决:自定义滚动选择器的选中项样式动态修改
核心问题分析
- 原代码中两个滑块共用同一个
offset绑定变量,导致滚动时互相干扰,无法独立判断各自的选中项 - 所有文本样式固定,未根据滚动偏移动态调整选中项的字体和颜色
- 滚动结束后的位置计算逻辑有误,无法保证选中项居中显示
修改后的完整代码
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
相关产品推荐
相关产品推荐

