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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 03:25:13