SwiftUI中如何检测ScrollView是否被拖拽?
解决方案
方案一:封装UIScrollView获取拖拽状态(推荐)
SwiftUI原生ScrollView未直接暴露isDragging/isDecelerating这类状态,最可靠的方式是封装UIKit的UIScrollView,直接获取原生拖拽属性:
1. 创建UIScrollView封装
import SwiftUI struct UIKitScrollView<Content: View>: UIViewRepresentable { @Binding var contentOffset: CGPoint @Binding var isDragging: Bool @Binding var isDecelerating: Bool let content: () -> Content func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() scrollView.delegate = context.coordinator 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.widthAnchor.constraint(equalTo: scrollView.widthAnchor) ]) return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) { // 仅非用户拖拽时更新滚动位置,避免循环 if !isDragging && !isDecelerating { uiView.contentOffset = contentOffset } } func makeCoordinator() -> Coordinator { Coordinator(parent: self) } class Coordinator: NSObject, UIScrollViewDelegate { let parent: UIKitScrollView init(parent: UIKitScrollView) { self.parent = parent } func scrollViewDidScroll(_ scrollView: UIScrollView) { parent.contentOffset = scrollView.contentOffset } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { parent.isDragging = true } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { parent.isDragging = false parent.isDecelerating = decelerate } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { parent.isDecelerating = false } } }
2. 在视图中使用封装
替换原生ScrollView,通过绑定的拖拽状态避免循环更新:
@Binding var scrollPositionOffset: CGFloat let zoomMultiplier: CGFloat = 1.5 @State private var contentOffset: CGPoint = .zero @State private var isDragging: Bool = false @State private var isDecelerating: Bool = false var body: some View { GeometryReader { geometry in UIKitScrollView( contentOffset: $contentOffset, isDragging: $isDragging, isDecelerating: $isDecelerating, content: { ZStack(alignment: .top) { // 你的ScrollView内容 MagnifierView() .frame(height: geometry.size.height * zoomMultiplier) } } ) .onAppear { let initialY = geometry.size.height * zoomMultiplier * -scrollPositionOffset contentOffset = CGPoint(x: 0, y: initialY) } .onChange(of: scrollPositionOffset) { newValue in if !isDragging && !isDecelerating { let targetY = geometry.size.height * zoomMultiplier * -newValue contentOffset = CGPoint(x: 0, y: targetY) } } .onChange(of: contentOffset.y) { newY in // 反向计算偏移量更新小地图 scrollPositionOffset = -newY / (geometry.size.height * zoomMultiplier) } } }
方案二:纯SwiftUI改进(依赖PreferenceKey监听滚动)
若不想依赖UIKit,可通过PreferenceKey监听滚动偏移,同时调整手势优先级捕获拖拽状态:
1. 定义滚动偏移监听Key
struct ScrollOffsetPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {} }
2. 调整视图代码
@Binding var scrollPositionOffset: CGFloat let zoomMultiplier: CGFloat = 1.5 @State private var localScrolling: Bool = false var body: some View { ScrollViewReader { scrollViewProxy in GeometryReader { geometry in ScrollView { ZStack(alignment: .top) { MagnifierView() .frame(height: geometry.size.height * zoomMultiplier) Rectangle() .frame(height: 10) .alignmentGuide(.top) { _ in geometry.size.height * zoomMultiplier * -scrollPositionOffset } .id("scrollOffset") .background( GeometryReader { proxy in Color.clear .preference( key: ScrollOffsetPreferenceKey.self, value: proxy.frame(in: .named("scrollView")).minY ) } ) } } .coordinateSpace(name: "scrollView") .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in scrollPositionOffset = -offset / (geometry.size.height * zoomMultiplier) } .onAppear { scrollViewProxy.scrollTo("scrollOffset", anchor: .top) } .onChange(of: scrollPositionOffset) { _ in if !localScrolling { scrollViewProxy.scrollTo("scrollOffset", anchor: .top) } } // 让自定义手势与ScrollView手势共存 .simultaneousGesture( DragGesture(minimumDistance: 0) .onChanged { _ in localScrolling = true } .onEnded { _ in DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { localScrolling = false } } ) } } }
小地图场景核心逻辑
两种方案的核心都是区分用户操作与程序操作:
- 用户拖拽小地图:更新
scrollPositionOffset,此时拖拽状态为false,触发程序式滚动 - 用户拖拽ScrollView:拖拽状态为true,忽略程序式滚动触发,同时反向计算偏移量更新小地图
内容的提问来源于stack exchange,提问作者Ben Frearson
相关产品推荐
相关产品推荐

