SwiftUI iOS14/15 下ScrollView遮挡下层Spacer下方Button无法点击问题问询
问题根源
该问题是SwiftUI中ScrollView的默认手势响应逻辑导致的:ScrollView会将自身bounds范围内的所有区域都纳入手势响应范围,即使内部的子视图设置了.allowsHitTesting(false),ScrollView本身的手势识别器依然会优先拦截点击事件,不会向下传递给ZStack中更下层的视图。
解决方案
方案1:调整内边距替代内部空白占位(全版本适配,成本最低)
如果你的ScrollView的空白区域仅用来做固定的边距偏移,不需要跟随内容滚动,直接将内部的占位Spacer替换为ScrollView自身的padding即可:
struct FixedView: View { var body: some View { ZStack { VStack { Button("Tap This") { print("Tapped") } Spacer() } VStack { ScrollView { Rectangle() .foregroundColor(.blue) .frame(height: 150) Spacer() } // 原内部Spacer的高度直接作为ScrollView的top padding .padding(.top, 150) } } } }
原理:原本被内部Spacer占据的顶部区域不再属于ScrollView的可交互范围,自然不会拦截下层按钮的点击事件,ScrollView的滚动功能完全不受影响。
方案2:自定义可透传点击的ScrollView(通用场景,全版本适配)
如果空白区域需要跟随内容滚动、或者位于内容中间无法用padding替代,可以用自定义ScrollView实现透明区域自动透传点击,同时保留滚动能力:
// 自定义可透传点击的ScrollView struct PassthroughScrollView<Content: View>: UIViewRepresentable { let content: Content let showsIndicators: Bool init(showsIndicators: Bool = true, @ViewBuilder content: () -> Content) { self.showsIndicators = showsIndicators self.content = content() } func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() scrollView.showsVerticalScrollIndicator = showsIndicators scrollView.showsHorizontalScrollIndicator = showsIndicators scrollView.backgroundColor = .clear // 嵌入SwiftUI内容 let hosting = UIHostingController(rootView: content) hosting.view.translatesAutoresizingMaskIntoConstraints = false hosting.view.backgroundColor = .clear scrollView.addSubview(hosting.view) NSLayoutConstraint.activate([ hosting.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), hosting.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), hosting.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), hosting.view.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), hosting.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor) ]) scrollView.passthroughDelegate = context.coordinator return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) {} func makeCoordinator() -> GestureCoordinator { GestureCoordinator() } class GestureCoordinator: NSObject, UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { // 仅当点击落在ScrollView的子视图上时才响应手势,否则透传给下层 let point = touch.location(in: gestureRecognizer.view) guard let hitView = gestureRecognizer.view?.hitTest(point, with: nil) else { return false } return hitView !== gestureRecognizer.view } } } private var delegateKey: UInt8 = 0 extension UIScrollView { var passthroughDelegate: GestureCoordinator? { get { objc_getAssociatedObject(self, &delegateKey) as? GestureCoordinator } set { objc_setAssociatedObject(self, &delegateKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) panGestureRecognizer.delegate = newValue tapGestureRecognizer?.delegate = newValue } } }
使用时直接替换原生ScrollView即可:
struct FixedView: View { var body: some View { ZStack { VStack { Button("Tap This") { print("Tapped") } Spacer() } VStack { PassthroughScrollView { Spacer() .frame(height: 150) Rectangle() .foregroundColor(.blue) .frame(height: 150) Spacer() } } } } }
两个方案在iOS14、iOS15及以上版本都可稳定运行。
内容的提问来源于stack exchange,提问作者TheDrewBenson
相关产品推荐
相关产品推荐

