SwiftUI:如何判断ScrollView停止滚动、处理回调并让选中项居中?
SwiftUI ScrollView 滚动状态判断与居中调整方案
1. 判断ScrollView是否已停止滚动
SwiftUI原生ScrollView没有直接提供滚动停止的回调,需通过桥接UIKit的UIScrollView利用其代理机制实现。核心是通过scrollViewDidEndDecelerating(滚动减速至停止时触发)和scrollViewDidEndDragging(_:willDecelerate:)(拖拽结束后,若willDecelerate为false则直接停止滚动)这两个代理方法来判定滚动停止状态。
2. 处理scrollViewDidEndDecelerating和scrollViewDidEndDragging方法
通过自定义UIViewRepresentable包装UIScrollView,并实现其代理逻辑,将滚动状态回调传递给SwiftUI视图:
- 创建
ScrollViewWithDelegate结构体,遵循UIViewRepresentable协议,在makeUIView方法中配置UIScrollView并设置代理。 - 内部嵌套
Coordinator类遵循UIScrollViewDelegate,实现两个目标代理方法,更新滚动状态并触发居中调整逻辑。
3. 滚动停止时让选中项居中
当滚动停止时,计算选中项对应的目标偏移量,调用UIScrollView的setContentOffset(_:animated:)方法调整位置,确保选中项中心与屏幕中心对齐:
- 编写
calculateTargetOffset方法,根据选中索引、项宽度、间距和屏幕宽度计算目标偏移值。 - 在代理方法中调用该方法,自动滚动到目标位置。
完整实现代码
import SwiftUI struct ScrollViewWithDelegate<Content: View>: UIViewRepresentable { let axis: Axis.Set let showsIndicators: Bool @Binding var isScrolling: Bool @Binding var selectedIndex: Int let itemWidth: CGFloat let spacing: CGFloat let content: () -> Content func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() scrollView.delegate = context.coordinator scrollView.showsHorizontalScrollIndicator = showsIndicators && axis.contains(.horizontal) scrollView.showsVerticalScrollIndicator = showsIndicators && axis.contains(.vertical) scrollView.isPagingEnabled = false let hostingController = UIHostingController(rootView: content()) hostingController.view.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(hostingController.view) NSLayoutConstraint.activate([ hostingController.view.topAnchor.constraint(equalTo: scrollView.topAnchor), hostingController.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), hostingController.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), hostingController.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), hostingController.view.heightAnchor.constraint(equalTo: scrollView.heightAnchor) ]) return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) { let targetOffset = calculateTargetOffset(for: selectedIndex) if uiView.contentOffset.x != targetOffset { uiView.setContentOffset(CGPoint(x: targetOffset, y: 0), animated: true) } } func makeCoordinator() -> Coordinator { Coordinator(parent: self) } class Coordinator: NSObject, UIScrollViewDelegate { let parent: ScrollViewWithDelegate init(parent: ScrollViewWithDelegate) { self.parent = parent } func scrollViewDidScroll(_ scrollView: UIScrollView) { parent.isScrolling = true let itemTotalWidth = parent.itemWidth + parent.spacing let index = Int(round(scrollView.contentOffset.x / itemTotalWidth)) parent.selectedIndex = max(0, min(index, parent.imageCount - 1)) } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { parent.isScrolling = false adjustToCenter(scrollView: scrollView) } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { parent.isScrolling = false adjustToCenter(scrollView: scrollView) } } private func adjustToCenter(scrollView: UIScrollView) { let targetOffset = parent.calculateTargetOffset(for: parent.selectedIndex) scrollView.setContentOffset(CGPoint(x: targetOffset, y: 0), animated: true) } } private var imageCount: Int { (content() as? HStack<AnyView>)?.children.count ?? 0 } private func calculateTargetOffset(for index: Int) -> CGFloat { let itemTotalWidth = itemWidth + spacing let screenWidth = UIScreen.main.bounds.width return (itemTotalWidth * CGFloat(index)) + (itemWidth / 2) - (screenWidth / 2) } } struct ContentView: View { @State private var imageList: [String] = ["Onur","Onur","Onur","Onur","Onur","Onur","Onur","Onur","Onur"] private let itemWidth: CGFloat = 100 private var spacing: CGFloat { UIScreen.main.bounds.width / 2 - itemWidth } @State private var isScrolling: Bool = false @State private var selectedIndex: Int = 0 var body: some View { ScrollViewWithDelegate( axis: .horizontal, showsIndicators: false, isScrolling: $isScrolling, selectedIndex: $selectedIndex, itemWidth: itemWidth, spacing: spacing ) { HStack(spacing: spacing) { Spacer().frame(width: UIScreen.main.bounds.width / 2 - itemWidth / 2) ForEach(Array(imageList.enumerated()), id: \.element) { index, image in Image(image) .resizable() .aspectRatio(contentMode: .fit) .frame(width: itemWidth, height: itemWidth) .clipShape(Circle()) .overlay(Circle().stroke(.blue, lineWidth: index == selectedIndex ? 4 : 0)) } Spacer().frame(width: UIScreen.main.bounds.width / 2 - itemWidth / 2) } .frame(height: itemWidth) } } }
代码说明
ScrollViewWithDelegate:桥接UIKit的UIScrollView,实现代理方法监听滚动状态,实时更新选中索引,并在滚动停止时自动调整选中项到屏幕中心。calculateTargetOffset:精准计算选中项需要的偏移量,确保其中心与屏幕中心对齐。ContentView:使用自定义滚动视图,调整左右边距保证首尾项也能居中显示,根据选中状态高亮对应项的边框。
内容的提问来源于stack exchange,提问作者onur.altintas
相关产品推荐
相关产品推荐

