SwiftUI自定义可拖拽Sheet中如何让DragGesture在List/ScrollView上正常生效
解决方案
核心问题是List/ScrollView自带的滚动手势优先级高于你添加的DragGesture,默认会直接吞掉拖拽事件,仅靠SwiftUI原生的手势优先级修饰符无法实现滚动优先+拖拽兼容的效果,可按以下方案修改:
方案核心逻辑
- 允许两个手势同时识别,不会互斥
- 监听内部滚动容器的偏移量,仅在滚动到顶部时才响应Sheet的拖拽调整
- 手势方向优先匹配当前场景:未到顶时拖拽全部交给滚动容器,到顶后纵向拖拽交给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的手势封装实现完全无侵入的效果:
- 用
UIViewRepresentable给Sheet的根View加自定义UIPanGestureRecognizer - 实现手势代理方法
gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)返回true,允许和内部滚动手势共存 - 在手势回调里通过遍历子视图找到内部的UIScrollView,直接读取它的
contentOffset判断是否到顶,再决定是否处理Sheet拖拽
内容的提问来源于stack exchange,提问作者Jopolaz
相关产品推荐
相关产品推荐

