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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 14:42:00