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

SwiftUI中如何修改响应者链流程?实现指定视图触控交互需求

SwiftUI实现滚动内容未覆盖区域的触摸穿透需求

需求描述

我需要实现这样的交互逻辑:视图初始化完成后,下方的generalFields视图在未被ScrollView内的additionalFields内容覆盖的区域,能够正常接收触控事件。ScrollView里放置一个透明度为0的generalFields是为了占位,保证下方真实的generalFields始终对用户可见。我知道这个逻辑在UIKit中可以实现,想了解能不能通过UIViewRepresentable、SwiftUIIntrospect这类工具在SwiftUI中达成效果。


简化代码示例

public var body: some View {
    VStack(spacing: 0) {
        ZStack {
            generalFields // 需要接收触摸的视图,不能被滚动内容遮挡可见性
            ScrollView(showsIndicators: false) {
                generalFields // 用于占位
                    .opacity(0)
                    .padding(.bottom, 16)
                    .allowsHitTesting(false)
                VStack(spacing: 500) {
                    additionalFields
                    additionalFields
                }
                .background(Color.white)
                .cornerRadius(16)
            }
        }
    }
    .background(Color.blue.ignoresSafeArea())
}

完整可复制代码

import Foundation
import SwiftUI

public struct TDCreateDirectionView: View {
    
    // MARK: - 属性
    
    @State var patientName = ""
    @State var patientDate = Date()
    @State var patientPhone = ""
    
    // MARK: - 视图
    
    public var body: some View {
        VStack(spacing: 0) {
            ZStack {
                generalFields // 需要接收触摸的视图,不能被滚动内容遮挡可见性
                ScrollView(showsIndicators: false) {
                    generalFields // 用于占位
                        .opacity(0)
                        .padding(.bottom, 16)
                        .allowsHitTesting(false)
                    VStack(spacing: 500) {
                        additionalFields
                        additionalFields
                    }
                    .background(Color.white)
                    .cornerRadius(16)
                }
            }
        }
        .background(Color.blue.ignoresSafeArea())
    }
    
    private var generalFields: some View {
        VStack(spacing: 0) {
            HStack {
                Text("患者信息")
                    .font(.system(size: 16, weight: .medium))
                    .foregroundColor(.white)
                Spacer()
            }
            .padding(.horizontal, 16)
            .padding(.top, 16)
            VStack(spacing: 8) {
                HStack {
                    TextField("姓名", text: $patientName)
                        .font(.system(size: 16, weight: .medium))
                }
                .padding(.vertical, 12)
                .padding(.horizontal, 16)
                .background(Color.white)
                .cornerRadius(8)
                Button {
                    
                } label: {
                    HStack {
                        Text("诊所")
                            .font(.system(size: 16, weight: .medium))
                            .foregroundColor(.secondary)
                    }
                    Spacer()
                }
                .padding(.vertical, 12)
                .padding(.horizontal, 16)
                .background(Color.white)
                .cornerRadius(8)
            }
            .padding(.top, 8)
            .padding(.horizontal, 16)
            VStack(spacing: 4) {
                HStack {
                    Text("补充信息")
                        .font(.system(size: 12))
                        .foregroundColor(.white)
                    Spacer()
                }
                .padding(.top, 16)
                HStack {
                    TextField("电话", text: $patientPhone)
                        .font(.system(size: 16, weight: .medium))
                }
                .padding(.vertical, 12)
                .padding(.horizontal, 16)
                .background(Color.white)
                .cornerRadius(8)
                Spacer()
            }
            .padding(.horizontal, 16)
        }
    }
    
    private var additionalFields: some View {
        VStack(spacing: 16) {
            HStack {
                Text("补充信息")
                    .font(.system(size: 12))
                    .foregroundColor(.black)
                Spacer()
            }
            .padding(.top, 16)
            HStack {
                TextField("电话", text: $patientPhone)
                    .font(.system(size: 16, weight: .medium))
                    .foregroundColor(.white)
            }
            .padding(.vertical, 12)
            .padding(.horizontal, 16)
            .background(Color.black)
            .cornerRadius(8)
            Button {
                
            } label: {
                HStack {
                    Text("按钮")
                        .font(.system(size: 16, weight: .medium))
                        .foregroundColor(.secondary)
                }
                Spacer()
            }
            .padding(.vertical, 12)
            .padding(.horizontal, 16)
            .background(Color.black)
            .cornerRadius(8)
            Spacer()
        }
        .padding(.horizontal, 16)
    }
}

#Preview {
    TDCreateDirectionView()
}

解决方案

方法一:自定义UIViewRepresentable实现触摸穿透的ScrollView

通过封装原生UIScrollView,重写触摸事件处理逻辑,让未被滚动内容覆盖的区域将触摸事件传递给下方的generalFields。

struct PassThroughScrollView<Content: View>: UIViewRepresentable {
    var content: () -> Content
    
    func makeUIView(context: Context) -> UIScrollView {
        let scrollView = UIScrollView()
        scrollView.showsVerticalScrollIndicator = false
        
        let hostingView = UIHostingController(rootView: content())
        hostingView.view.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(hostingView.view)
        
        NSLayoutConstraint.activate([
            hostingView.view.topAnchor.constraint(equalTo: scrollView.topAnchor),
            hostingView.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            hostingView.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            hostingView.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            hostingView.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
        ])
        
        return scrollView
    }
    
    func updateUIView(_ uiView: UIScrollView, context: Context) {
        let hostingView = uiView.subviews.first as! UIHostingController<Content>
        hostingView.rootView = content()
    }
    
    // 自定义触摸穿透的视图容器
    class PassThroughContainer: UIView {
        override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
            let hitView = super.hitTest(point, with: event)
            // 如果触摸点仅命中容器本身,返回nil让事件穿透到下层
            return hitView == self ? nil : hitView
        }
    }
}

使用时替换原有ScrollView:

PassThroughScrollView {
    generalFields // 用于占位
        .opacity(0)
        .padding(.bottom, 16)
        .allowsHitTesting(false)
    VStack(spacing: 500) {
        additionalFields
        additionalFields
    }
    .background(Color.white)
    .cornerRadius(16)
}

方法二:使用SwiftUIIntrospect修改原生UIScrollView

通过SwiftUIIntrospect获取原生UIScrollView,替换其容器视图实现触摸穿透。

首先导入依赖:

import SwiftUIIntrospect

修改原有ScrollView代码:

ScrollView(showsIndicators: false) {
    generalFields // 用于占位
        .opacity(0)
        .padding(.bottom, 16)
        .allowsHitTesting(false)
    VStack(spacing: 500) {
        additionalFields
        additionalFields
    }
    .background(Color.white)
    .cornerRadius(16)
}
.introspect(.scrollView, on: .iOS(.v13...)) { scrollView in
    // 创建触摸穿透容器
    class TouchPassThroughView: UIView {
        override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
            let hit = super.hitTest(point, with: event)
            return hit == self ? nil : hit
        }
    }
    
    // 替换原容器视图
    guard let originalHostView = scrollView.subviews.first else { return }
    originalHostView.removeFromSuperview()
    
    let passThroughView = TouchPassThroughView()
    passThroughView.translatesAutoresizingMaskIntoConstraints = false
    scrollView.addSubview(passThroughView)
    
    passThroughView.addSubview(originalHostView)
    NSLayoutConstraint.activate([
        passThroughView.topAnchor.constraint(equalTo: scrollView.topAnchor),
        passThroughView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
        passThroughView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
        passThroughView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
        passThroughView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
        
        originalHostView.topAnchor.constraint(equalTo: passThroughView.topAnchor),
        originalHostView.leadingAnchor.constraint(equalTo: passThroughView.leadingAnchor),
        originalHostView.trailingAnchor.constraint(equalTo: passThroughView.trailingAnchor),
        originalHostView.bottomAnchor.constraint(equalTo: passThroughView.bottomAnchor),
        originalHostView.widthAnchor.constraint(equalTo: passThroughView.widthAnchor)
    ])
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 15:43:11