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

SwiftUI中为偏移后空白区域添加点击手势以收起键盘

问题:偏移产生的空白区域无法响应点击收起键盘

我给TextField实现了键盘显示时的Y轴偏移机制,触发时整个屏幕会上移。现在希望点击任意空白区域即可收起键盘,但偏移产生的空白区域无法响应点击手势,试过在各处添加点击手势识别器都没有效果。

原代码如下:

struct LoginView: View {
    @State private var email = ""
    @State private var password = ""

    @State private var keyboardYOffset = CGFloat(0)

    var body: some View {
        VStack {
            VStack(spacing: 32) {

             Rectangle()
                .frame(width: 187, height: 208)
                .padding(.top, 56)
                .padding(.bottom, 94)
                .background(Color.red)

                TextField("Email", text: $email)
                    .frame(maxWidth: .infinity)
                    .frame(height: 50)
                // inset of the placeholder text
                    .padding(.horizontal)
                    .border(Color.red)
                // inset of the entire textfield
                    .padding(.horizontal)


                SecureField("Password", text: $email)
                    .frame(maxWidth: .infinity)
                    .frame(height: 50)
                // inset of the placeholder text
                    .padding(.horizontal)
                    .border(Color.red)
                // inset of the entire textfield
                    .padding(.horizontal)


                Spacer()
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .padding(.top, 32)
            .background(Color.white.ignoresSafeArea())
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
        .background(Theme.BrandColor.primary.ignoresSafeArea())
        .offset(y: -keyboardYOffset)
        .animation(.easeInOut(duration: 1), value: keyboardYOffset)
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
            let value = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
            let keyboardHeight = value?.height ?? 0
            // for our purposes we only need to offset by half the keyboard height
            self.keyboardYOffset = keyboardHeight / 2
        }
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
            self.keyboardYOffset = 0
        }
        .onTapGesture {
            UIApplication.shared.endEditing()
        }
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView()
    }
}

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

解决方案

原因分析

当前的onTapGesture绑定在最外层VStack上,当屏幕上移后,VStack下方露出的空白区域并不属于这个VStack的点击范围,所以点击该区域不会触发手势。

方法一:用ZStack分层处理全屏点击

把全屏背景和内容视图分层,让底层的全屏背景接收所有点击事件,这样不管内容怎么偏移,空白区域的点击都能被捕获:

struct LoginView: View {
    @State private var email = ""
    @State private var password = ""

    @State private var keyboardYOffset = CGFloat(0)

    var body: some View {
        ZStack {
            // 底层全屏背景,接收所有空白区域点击
            Theme.BrandColor.primary
                .ignoresSafeArea()
                .onTapGesture {
                    UIApplication.shared.endEditing()
                }
            
            // 原有内容视图
            VStack {
                VStack(spacing: 32) {
                    Rectangle()
                        .frame(width: 187, height: 208)
                        .padding(.top, 56)
                        .padding(.bottom, 94)
                        .background(Color.red)

                    TextField("Email", text: $email)
                        .frame(maxWidth: .infinity)
                        .frame(height: 50)
                        .padding(.horizontal)
                        .border(Color.red)
                        .padding(.horizontal)

                    // 修复原代码的绑定错误:SecureField应该绑定password
                    SecureField("Password", text: $password)
                        .frame(maxWidth: .infinity)
                        .frame(height: 50)
                        .padding(.horizontal)
                        .border(Color.red)
                        .padding(.horizontal)

                    Spacer()
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .padding(.top, 32)
                .background(Color.white.ignoresSafeArea())
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
            .offset(y: -keyboardYOffset)
            .animation(.easeInOut(duration: 1), value: keyboardYOffset)
        }
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
            let value = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
            let keyboardHeight = value?.height ?? 0
            self.keyboardYOffset = keyboardHeight / 2
        }
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
            self.keyboardYOffset = 0
        }
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView()
    }
}

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

方法二:扩展视图的点击区域

给最外层VStack添加contentShape(Rectangle()),强制让视图的点击区域扩展到整个屏幕范围,即使偏移后,空白区域也会被视为视图的一部分:

struct LoginView: View {
    @State private var email = ""
    @State private var password = ""

    @State private var keyboardYOffset = CGFloat(0)

    var body: some View {
        VStack {
            // 原有内容视图
            VStack(spacing: 32) {
                Rectangle()
                    .frame(width: 187, height: 208)
                    .padding(.top, 56)
                    .padding(.bottom, 94)
                    .background(Color.red)

                TextField("Email", text: $email)
                    .frame(maxWidth: .infinity)
                    .frame(height: 50)
                    .padding(.horizontal)
                    .border(Color.red)
                    .padding(.horizontal)

                // 修复绑定错误
                SecureField("Password", text: $password)
                    .frame(maxWidth: .infinity)
                    .frame(height: 50)
                    .padding(.horizontal)
                    .border(Color.red)
                    .padding(.horizontal)

                Spacer()
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .padding(.top, 32)
            .background(Color.white.ignoresSafeArea())
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
        .background(Theme.BrandColor.primary.ignoresSafeArea())
        .offset(y: -keyboardYOffset)
        .animation(.easeInOut(duration: 1), value: keyboardYOffset)
        // 添加这一行,扩展点击区域到整个屏幕
        .contentShape(Rectangle())
        .onTapGesture {
            UIApplication.shared.endEditing()
        }
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
            let value = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
            let keyboardHeight = value?.height ?? 0
            self.keyboardYOffset = keyboardHeight / 2
        }
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
            self.keyboardYOffset = 0
        }
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView()
    }
}

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

额外修复

原代码中SecureField错误绑定了email状态变量,已在上述代码中修正为绑定password,避免输入密码时同步修改邮箱内容。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 11:04:57