iOS16+ Swift登录页:添加隐藏键盘手势后切换输入框键盘闪跳问题
解决SwiftUI切换输入框时键盘闪烁问题
问题根源在于全局的.onTapGesture会拦截所有点击事件,包括切换输入框时的点击操作。点击第二个输入框时,手势先触发隐藏键盘,随后输入框获得焦点重新唤起键盘,导致出现闪烁现象。
解决方案:自定义点击外部隐藏键盘的ViewModifier
通过自定义ViewModifier结合UIKit手势识别,判断点击区域是否为输入框,仅在点击非输入框区域时隐藏键盘:
import SwiftUI // 自定义Modifier,实现点击非输入框区域隐藏键盘 struct DismissKeyboardOnTapOutside: ViewModifier { func body(content: Content) -> some View { content .background(TapOutsideDetector()) } } // 手势检测视图,判断点击区域是否为输入框 struct TapOutsideDetector: UIViewRepresentable { func makeUIView(context: Context) -> UIView { let view = UIView() view.backgroundColor = .clear 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(_ gesture: UITapGestureRecognizer) { let tapPoint = gesture.location(in: gesture.view) guard let hitView = gesture.view?.hitTest(tapPoint, with: nil) else { return } // 判断点击的是否为UITextField或其子视图,若是则不隐藏键盘 if !(hitView is UITextField || hitView.isDescendant(of: UITextField())) { UIApplication.shared.sendAction( #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil ) } } } func makeCoordinator() -> Coordinator { Coordinator() } } // 给View添加扩展,方便调用 extension View { func dismissKeyboardOnTapOutside() -> some View { self.modifier(DismissKeyboardOnTapOutside()) } }
修改你的LoginView代码
移除原来的.onTapGesture { self.hideKeyboard() },替换为自定义的Modifier:
struct LoginView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @State var email: String = "" @State var password: String = "" private let hapticFeedbackService = HapticFeedbackService() var body: some View { GeometryReader { screenGeometry in let screenHeight = screenGeometry.size.height ZStack { Color.black.background.ignoresSafeArea(.all) // 原有页面内容保持不变 VStack { WelcomeTextView(welcomeText: "sign_in_to") .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, screenHeight * 0.02) VStack { InputField( text: $email, placeholder: "enter_email", isSecure: false ) .padding(.bottom, 15) InputField( text: $password, placeholder: "enter_password", isSecure: true, trailingIcon: Image("Lock") ) } Button(action: { hapticFeedbackService.triggerHapticFeedback() print("Button pressed") }) { Text(LocalizedStringKey("forgot_your_password")) .font(.thinnerSmallTitle) } .padding(.top, 5) .padding(.trailing, 5) .frame(maxWidth: .infinity, alignment: .trailing) LargeButton( text: "sign_in", textColor: Color.black, backgroundColor: Color.white ) { hapticFeedbackService.triggerHapticFeedback() print("Button pressed") } .padding(.top, 40) Text(LocalizedStringKey("other_login_options")) .font(.mediumText) .padding(.top, 10) HStack { SSOButton(type: .Apple, action: { hapticFeedbackService.triggerHapticFeedback() print("Button pressed") }) .padding(.trailing, 5) SSOButton(type: .Google, action: { hapticFeedbackService.triggerHapticFeedback() print("Button pressed") }) .padding(.leading, 5) } .padding(.top, 10) HStack { Text(LocalizedStringKey("dont_have_account")) .font(.mediumText) .foregroundColor(Color.black) Button(action: { hapticFeedbackService.triggerHapticFeedback() print("Button pressed") }) { Text(LocalizedStringKey("dont_have_account_sign_up")) .font(.smallTitle) .foregroundColor(Color.black) } } .padding(.top, 25) Spacer() } .paddedFrame( screenSize: screenGeometry.size, horizontalSizeClass: horizontalSizeClass ) } } .ignoresSafeArea(.keyboard) // 替换原来的onTapGesture为自定义Modifier .dismissKeyboardOnTapOutside() } }
原理说明
- 自定义的
TapOutsideDetector添加了不取消触摸事件传递的手势识别器,确保输入框能正常响应点击。 - 点击时检测目标视图是否为
UITextField或其子视图,若是则跳过隐藏键盘操作。 - 仅当点击非输入框区域时,才调用
resignFirstResponder隐藏键盘。
这样切换输入框时,点击操作会被输入框正常响应,不会触发键盘隐藏,完美保留原生切换输入框的流畅体验。
内容的提问来源于stack exchange,提问作者Petr Jelínek
相关产品推荐
相关产品推荐

