iOS 17 SwiftUI中如何检测ScrollView分页滚动结束
iOS 17 分页滚动动画结束后的回调方案
针对你用.scrollTargetBehavior(.paging)的场景,iOS 17 提供了两个精准的解决方案,不用折腾复杂的UIKit桥接或者不靠谱的延时监听:
方案一:SwiftUI原生绑定+动画完成回调
利用iOS17新增的scrollPosition绑定和withAnimation的completion闭包,直接监听滚动动画结束:
struct PagingDemoView: View { let pageCount = 5 @State private var currentPage: Int? = 0 @State private var isAnimatingScroll = false var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: 0) { ForEach(0..<pageCount, id: \.self) { page in Color.random .frame(width: UIScreen.main.bounds.width, height: 300) .id(page) } } .scrollTargetLayout() } .scrollTargetBehavior(.paging) .scrollPosition(id: $currentPage) .onChange(of: currentPage) { oldPage, newPage in guard oldPage != newPage, let newPage = newPage else { return } isAnimatingScroll = true withAnimation { currentPage = newPage } completion: { isAnimatingScroll = false // 这里就是动画完全结束后的回调逻辑 print("页面切换完成,当前页码:\(newPage)") } } } } // 辅助扩展:生成随机颜色 extension Color { static var random: Color { Color(red: Double.random(in: 0...1), green: Double.random(in: 0...1), blue: Double.random(in: 0...1)) } }
方案二:UIKit代理原生回调(复杂场景适配)
如果需要完全复用scrollViewDidEndScrollingAnimation的原生逻辑,通过SwiftUI包裹UIScrollView实现:
struct PagingScrollView<Content: View>: UIViewRepresentable { let content: Content let onAnimationEnd: () -> Void init(onAnimationEnd: @escaping () -> Void, @ViewBuilder content: () -> Content) { self.onAnimationEnd = onAnimationEnd self.content = content() } func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() scrollView.isPagingEnabled = true scrollView.delegate = context.coordinator // 将SwiftUI内容包装成UIKit视图 let hostingView = UIHostingController(rootView: content).view! hostingView.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(hostingView) NSLayoutConstraint.activate([ hostingView.topAnchor.constraint(equalTo: scrollView.topAnchor), hostingView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), hostingView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), hostingView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), hostingView.heightAnchor.constraint(equalTo: scrollView.heightAnchor) ]) return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(parent: self) } class Coordinator: NSObject, UIScrollViewDelegate { let parent: PagingScrollView init(parent: PagingScrollView) { self.parent = parent } func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { parent.onAnimationEnd() } } } // 使用示例 struct ContentView: View { var body: some View { PagingScrollView(onAnimationEnd: { print("滚动动画完全结束,可执行后续逻辑") }) { LazyHStack(spacing: 0) { ForEach(0..<5, id: \.self) { _ in Color.random .frame(width: UIScreen.main.bounds.width, height: 300) } } } } }
- 方案一是iOS17专属的SwiftUI原生方案,代码简洁,适合绝大多数分页场景,能确保仅在动画完全结束后触发回调;
- 方案二通过UIKit桥接,完全复用原生代理方法,适合需要更精细控制的复杂场景;
- 两种方案都不会在滚动过程中或中途触发回调,完全匹配你的需求。
内容的提问来源于stack exchange,提问作者KAMIKAZE
相关产品推荐
相关产品推荐

