SwiftUI 按钮在键盘关闭时出现故障

SwiftUI button glitches on keyboard dismiss

我的视图中有一个按钮,按下时会调用一个 hideKeyboard 函数,如下所示:

func hideKeyboard() {
    let resign = #selector(UIResponder.resignFirstResponder)
    UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}

但是,当按下按钮并关闭键盘时,会出现一个故障,即按钮停留在原位而视图向下移动: https://giphy.com/gifs/Z7qCDpRSGoOb9CbVRQ

可重现的例子:

import SwiftUI


struct TestView: View {
    @State private var username: String = ""
    @State private var password: String = ""
    @State private var isAnimating: Bool = false

    var lightGrey = Color(red: 239.0/255.0,
                                      green: 243.0/255.0,
                                      blue: 244.0/255.0)
    
    var body: some View {
        ZStack() {
            VStack() {
                Spacer()
                Text("Text")
                    .font(.title)
                    .fontWeight(.semibold)
                    .padding(.bottom, 15)
                    .frame(maxWidth: .infinity, alignment: .leading)
                Text("Text")
                    .font(.subheadline)
                    .padding(.bottom)
                    .frame(maxWidth: .infinity, alignment: .leading)
                TextField("username", text: $username)
                    .padding()
                    .background(self.lightGrey)
                    .cornerRadius(5.0)
                    .padding(.bottom, 20)
                SecureField("password", text: $password)
                    .padding()
                    .background(self.lightGrey)
                    .cornerRadius(5.0)
                    .padding(.bottom, 20)
                Button(action: {
                    self.hideKeyboard()
                    login()
                })
                {
                    if isAnimating {
                        ProgressView()
                            .colorScheme(.dark)
                            .font(.headline)
                            .foregroundColor(.white)
                            .padding()
                            .frame(maxWidth: .infinity)
                            .background(Color.green)
                            .cornerRadius(10.0)
                            .padding(.bottom, 20)
                    }
                    else {
                        Text("Text")
                            .font(.headline)
                            .foregroundColor(.white)
                            .padding()
                            .frame(maxWidth: .infinity)
                            .background(Color.green)
                            .cornerRadius(10.0)
                            .padding(.bottom, 20)
                    }
                }
                .disabled(username.isEmpty || password.isEmpty || isAnimating)
                Text("Text")
                    .font(.footnote)
                    .frame(maxWidth: .infinity, alignment:.leading)
                Spacer()
            }
            .padding()
            .padding(.bottom, 150)
            .background(Color.white)
        }
    }
    
    func hideKeyboard() {
        let resign = #selector(UIResponder.resignFirstResponder)
        UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
    }
    
    func login() {
        isAnimating = true
    }
}

struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
    }
}

这是由于按钮里面的视图替换,下面找到固定的重新布局。

使用 Xcode 13.2 / iOS 15.2 进行测试(为演示激活了慢动画)

    Button(action: {
        self.hideKeyboard()
        login()
    })
    {
        VStack {                 // << persistent container !!
            if isAnimating {
                ProgressView()
                    .colorScheme(.dark)
            }
            else {
                Text("Text")
            }
        }
        .foregroundColor(.white)
        .font(.headline)
        .padding()
        .frame(maxWidth: .infinity)
        .background(Color.green)
        .cornerRadius(10.0)
        .padding(.bottom, 20)
    }
    .disabled(username.isEmpty || password.isEmpty || isAnimating)

看起来 UIApplication.shared.sendAction 和 SwiftUI 之间存在冲突:隐藏键盘会启动动画,但更新 isAnimating 属性 会重置它。

更改调用顺序解决问题:

Button(action: {
    login()
    self.hideKeyboard()
})