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

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()
    }
}

原理说明

  1. 自定义的TapOutsideDetector添加了不取消触摸事件传递的手势识别器,确保输入框能正常响应点击。
  2. 点击时检测目标视图是否为UITextField或其子视图,若是则跳过隐藏键盘操作。
  3. 仅当点击非输入框区域时,才调用resignFirstResponder隐藏键盘。

这样切换输入框时,点击操作会被输入框正常响应,不会触发键盘隐藏,完美保留原生切换输入框的流畅体验。

内容的提问来源于stack exchange,提问作者Petr Jelínek

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 15:40:56