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

SwiftUI自定义可拖拽Sheet中如何让DragGesture在List/ScrollView上正常生效

解决方案

核心问题是List/ScrollView自带的滚动手势优先级高于你添加的DragGesture,默认会直接吞掉拖拽事件,仅靠SwiftUI原生的手势优先级修饰符无法实现滚动优先+拖拽兼容的效果,可按以下方案修改:


方案核心逻辑

  1. 允许两个手势同时识别,不会互斥
  2. 监听内部滚动容器的偏移量,仅在滚动到顶部时才响应Sheet的拖拽调整
  3. 手势方向优先匹配当前场景:未到顶时拖拽全部交给滚动容器,到顶后纵向拖拽交给Sheet调整高度

完整修改代码

第一步:定义滚动偏移监听的PreferenceKey

// 用于监听内部ScrollView/List的滚动偏移
struct ScrollOffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

// Sheet模式枚举,补全方便运行
enum YLSheetMode {
    case quarter, half, full
}

第二步:修改YLFlexSheet实现

public struct YLFlexSheet<Content: View>: View {
    
    @State private var dragOffset = CGSize.zero
    // 存储内部滚动容器的偏移量
    @State private var scrollViewOffset: CGFloat = 0
    
    private let content: () -> Content
    private let draggable: Bool
    @Binding private var sheetMode: YLSheetMode
    
    // 不同模式对应的offset计算逻辑,可替换为你原有实现
    private var offSet: CGFloat {
        let baseOffset: CGFloat
        switch sheetMode {
        case .quarter: baseOffset = UIScreen.main.bounds.height * 0.75
        case .half: baseOffset = UIScreen.main.bounds.height * 0.5
        case .full: baseOffset = 0
        }
        return baseOffset + dragOffset.height
    }
    
    public init(sheetMode: Binding<YLSheetMode>,
                draggable: Bool = false,
                @ViewBuilder content: @escaping () -> Content) {
        self.content = content
        self._sheetMode = sheetMode
        self.draggable = draggable
    }
    
    // 原有模式切换逻辑保留即可
    private func updateMode(translationHeight: CGFloat) {
        let threshold: CGFloat = 100
        switch sheetMode {
        case .full:
            if translationHeight > threshold {
                sheetMode = .half
            }
        case .half:
            if translationHeight > threshold {
                sheetMode = .quarter
            } else if translationHeight < -threshold {
                sheetMode = .full
            }
        case .quarter:
            if translationHeight < -threshold {
                sheetMode = .half
            }
        }
    }
    
    public var body: some View {
        Group {
            if draggable {
                content()
                    // 监听内部滚动容器的偏移,可封装为扩展简化使用者调用
                    .background(
                        GeometryReader { proxy in
                            Color.clear
                                .preference(key: ScrollOffsetKey.self, value: proxy.frame(in: .named("scrollCoord")).minY)
                        }
                    )
                    .onPreferenceChange(ScrollOffsetKey.self, perform: { value in
                        scrollViewOffset = value
                    })
                    .coordinateSpace(name: "scrollCoord")
                    // 用simultaneousGesture让拖拽手势和滚动手势同时识别
                    .simultaneousGesture(
                        DragGesture(minimumDistance: 30, coordinateSpace: .global)
                            .onChanged { value in
                                // 仅当滚动到顶部时才处理Sheet拖拽
                                guard scrollViewOffset <= 0 else { return }
                                dragOffset = value.translation
                            }
                            .onEnded{ value in
                                dragOffset = .zero
                                guard scrollViewOffset <= 0 else { return }
                                updateMode(translationHeight: value.translation.height)
                            }
                    )
            }else{
                content()
            }
        }
        .cornerRadius(20)
        .offset(y: offSet > 0 ? offSet : 0)
        .animation(.spring(), value: offSet)
        .edgesIgnoringSafeArea(.all)
    }
}

无侵入优化方案(可选)

如果不想要求使用者给内部的List/ScrollView加监听代码,可以用UIKit的手势封装实现完全无侵入的效果:

  1. 用UIViewRepresentable给Sheet的根View加自定义UIPanGestureRecognizer
  2. 实现手势代理方法gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)返回true,允许和内部滚动手势共存
  3. 在手势回调里通过遍历子视图找到内部的UIScrollView,直接读取它的contentOffset判断是否到顶,再决定是否处理Sheet拖拽

内容的提问来源于stack exchange,提问作者Jopolaz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 14:15:04