SwiftUI问题:父视图Tap手势致SwipeActions失效,FocusState不稳定
原实现及冲突现象
我在iOS应用中用VStack包裹了带List的视图和带TextField的视图,代码如下:
var body: some View { NavigationView { ScrollViewReader { scrollProxy in VStack() { NoteListView(notes: notes) .onTapGesture { hideKeyboard() } NoteInputView(scrollProxy: scrollProxy) } } .navigationBarTitle(Text("Notes")) } } func hideKeyboard() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
在NoteListView中,我给列表项添加了左滑删除的swipeAction:
List(notes) { note in NotePreview(note: note) .swipeActions(edge: .leading, allowsFullSwipe: true) { Button(role: .destructive) { delete(note) } label: { Label("Delete", systemImage: "trash") } .tint(.red) } }
这个实现能正常隐藏键盘,但会导致swipeAction里的删除按钮点击手势无法被识别。试过用.simultaneousGesture、给按钮加.highPriorityGesture都没用,只有移除父视图的Tap手势,删除按钮才能正常响应,这是SwiftUI手势优先级机制导致的问题。
尝试的不稳定Workaround
我换成ZStack替代VStack,只在键盘显示时让Spacer捕获点击事件来隐藏键盘:
var body: some View { NavigationView { ScrollViewReader { scrollProxy in ZStack() { NoteListView(notes: notes) NoteInputView(scrollProxy: scrollProxy) } } .navigationBarTitle(Text("Notes")) } }
在NoteInputView里用@FocusState跟踪TextField的焦点状态:
struct NoteInputView: View { ... @FocusState var focusInputField: Bool var body: some View { ZStack(alignment: .bottom) { if(focusInputField) { Spacer() .frame(maxWidth: .infinity, maxHeight: .infinity) .contentShape(Rectangle()) .simultaneousGesture(TapGesture().onEnded({ _ in focusInputField = false print("I'm still standing yeah yeah yeah") print(focusInputField) })) } else { Spacer() .frame(maxWidth: .infinity, maxHeight: .infinity) } HStack() { if(focusInputField) { Button("", systemImage: "keyboard.chevron.compact.down") { focusInputField = false } } TextField("Enter a quick note...", text: $newNoteContent, axis: .vertical) .lineLimit(1...5) } } } }
但这个方案不稳定:有时候明明focusInputField是false,捕获点击的Spacer还是会触发打印,目前没法稳定复现这个问题。
可行的解决方法及原因分析
问题根源
SwiftUI的手势识别遵循从父视图到子视图的冒泡机制,父视图的.onTapGesture会先被识别,直接拦截所有子视图的手势事件,包括swipeAction按钮的点击,导致子视图手势无法触发。
解决方法1:仅在TextField聚焦时启用父视图Tap手势
通过@FocusState传递焦点状态,只在键盘弹出时给NoteListView添加隐藏键盘的Tap手势:
// 父视图中添加焦点绑定 @FocusState private var isTextFieldFocused: Bool var body: some View { NavigationView { ScrollViewReader { scrollProxy in VStack() { NoteListView(notes: notes) .onTapGesture { if isTextFieldFocused { hideKeyboard() } } NoteInputView(scrollProxy: scrollProxy, isTextFieldFocused: $isTextFieldFocused) } } .navigationBarTitle(Text("Notes")) } } // 修改NoteInputView,绑定焦点状态 struct NoteInputView: View { let scrollProxy: ScrollViewProxy @Binding var isTextFieldFocused: Bool @FocusState var focusInputField: Bool @Binding var newNoteContent: String var body: some View { HStack() { if focusInputField { Button("", systemImage: "keyboard.chevron.compact.down") { focusInputField = false } } TextField("Enter a quick note...", text: $newNoteContent, axis: .vertical) .lineLimit(1...5) .focused($focusInputField) .onChange(of: focusInputField) { newValue in isTextFieldFocused = newValue } } } }
这样只有在键盘弹出时,父视图的Tap手势才会生效,平时不拦截子视图的手势,swipeAction的删除按钮就能正常响应。
解决方法2:用UIKit手势避免拦截子视图事件
创建自定义UIViewRepresentable,添加Tap手势并设置cancelsTouchesInView为false,让手势只触发隐藏键盘,不拦截子视图手势:
struct TapToDismissKeyboardView: UIViewRepresentable { func makeUIView(context: Context) -> UIView { let view = UIView() let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap)) tapGesture.cancelsTouchesInView = false view.addGestureRecognizer(tapGesture) return view } func updateUIView(_ uiView: UIView, context: Context) {} class Coordinator: NSObject { @objc func handleTap() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } func makeCoordinator() -> Coordinator { Coordinator() } }
然后在父视图中用ZStack把这个视图叠在NoteListView上方,仅在TextField聚焦时显示:
@FocusState private var isTextFieldFocused: Bool var body: some View { NavigationView { ScrollViewReader { scrollProxy in ZStack { NoteListView(notes: notes) if isTextFieldFocused { TapToDismissKeyboardView() } NoteInputView(scrollProxy: scrollProxy, isTextFieldFocused: $isTextFieldFocused) } } .navigationBarTitle(Text("Notes")) } }
这个方法利用UIKit的手势属性,完美解决手势冲突问题。
内容的提问来源于stack exchange,提问作者hko

