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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 13:35:06