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
相关产品推荐
相关产品推荐

