为什么在 iOS14或至少 iOS14.4中,keyboardWillShowNotification 被触发两次,而在 iOS13 中只触发一次?

问题描述

为什么在 iOS14(或至少 iOS14.4 iPhone 12 模拟器)中,keyboardWillShowNotification 被触发两次,而在 iOS13 中只触发一次?

我的最终目标是使键盘隐藏的可见文本字段。

如果我按照以下步骤操作,这会使我的表单在 iOS14 上行为不端:

  1. 点按最顶部的文本字段。
  2. 滚动到底部
  3. 点按最后一个文本字段。

请注意,视图向上滚动到上一个点击的文本字段(在本例中是最顶部的)。

用户界面:

enter image description here

代码

class ViewController: UIViewController {
  @IBOutlet weak var scrollView: UIScrollView!

  override func viewDidLoad() {
    super.viewDidLoad()
  }
  
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    register()
  }

  override func viewWilldisappear(_ animated: Bool) {
    super.viewWilldisappear(animated)
    unregister()
  }
  
  func register() {
    NotificationCenter.default.addobserver(self,selector: #selector(keyboardWillShow(notification:)),name: UIResponder.keyboardWillShowNotification,object: nil)
    NotificationCenter.default.addobserver(self,selector: #selector(keyboardWillHide(notification:)),name: UIResponder.keyboardWillHideNotification,object: nil)
  }

  func unregister() {
    NotificationCenter.default.removeObserver(self,object: nil)
    NotificationCenter.default.removeObserver(self,object: nil)
  }

  @objc func keyboardWillShow(notification: NSNotification) {
    let keyboardFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)
    let keyboardSize = keyboardFrame?.size
    print("keyboardWillShow " + String(describing: keyboardSize))
    
    let contentInsets = UIEdgeInsets(top: 0.0,left: 0.0,bottom: keyboardSize!.height,right: 0.0)
    scrollView.contentInset = contentInsets
    scrollView.scrollIndicatorInsets = contentInsets
  }

  @objc func keyboardWillHide(notification: NSNotification) {
    print("keyboardWillHide")
    
    let contentInsets = UIEdgeInsets(top: 0.0,bottom: 0.0,right: 0.0)
    scrollView.contentInset = contentInsets
    scrollView.scrollIndicatorInsets = contentInsets
  }
}

输出

keyboardWillShow Optional((390.0,336.0))
keyboardWillShow Optional((390.0,336.0))

一个输出是当我点击最顶部的文本字段时。

输出 2 和 3 是当我点击底部的文本字段时。在这 2 个中,第一个对应于最顶部的文本字段,第二个对应于底部的文本字段。

并且滚动会自动滚动到最顶部的文本字段,而不是根本不滚动,因为用户点击了底部的文本字段。

解决方法

iOS 14 中似乎存在一个(或两个)错误(我正在 14.3 上进行测试)。

试试这个例子 - 它添加了一个全尺寸滚动视图,其中包含一个垂直堆栈视图,其中 8 个文本字段间隔 75 磅:

class KeyBoardTestViewController: UIViewController {
    
    let scrollView = UIScrollView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let stack = UIStackView()
        stack.axis = .vertical
        stack.spacing = 75
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        let safeG = view.safeAreaLayoutGuide
        let contentG = scrollView.contentLayoutGuide
        
        scrollView.addSubview(stack)
        view.addSubview(scrollView)
        
        NSLayoutConstraint.activate([
            
            scrollView.topAnchor.constraint(equalTo: safeG.topAnchor),scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor),stack.topAnchor.constraint(equalTo: contentG.topAnchor,constant: 20.0),stack.leadingAnchor.constraint(equalTo: contentG.leadingAnchor,stack.trailingAnchor.constraint(equalTo: contentG.trailingAnchor,stack.bottomAnchor.constraint(equalTo: contentG.bottomAnchor,constant: -20.0),stack.widthAnchor.constraint(equalToConstant: 200.0),])
        
        for i in 1...8 {
            let tf = UITextField()
            tf.borderStyle = .roundedRect
            tf.text = "\(i)"
            stack.addArrangedSubview(tf)
        }
        
    }
}

请注意没有键盘显示/隐藏处理。

当你运行它时,点击顶部的文本字段(里面有一个“1”)。键盘将显示。

现在,无需输入任何内容,向下滚动...因为我们没有调整滚动视图的内容插入,您将只能向下滚动到第 5 个或第 6 个文本字段。

点击最低的可见文本字段 --- 滚动视图将滚动,因此顶部的文本字段再次可见,而“文本字段 5”仍然是第一响应者。

如果您滚动/点击/滚动/点击...键盘将保持可见,但上一个第一响应者文本字段将滚动到视图中。

但是,只要您在字段中输入任何内容,一切都会恢复正常。

现在,我们将使用相同的类,但我们将添加键盘通知处理:

class KeyBoardTestViewController: UIViewController {
    
    let scrollView = UIScrollView()
    var firstTextField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let stack = UIStackView()
        stack.axis = .vertical
        stack.spacing = 75
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        let safeG = view.safeAreaLayoutGuide
        let contentG = scrollView.contentLayoutGuide
        
        scrollView.addSubview(stack)
        view.addSubview(scrollView)
        
        NSLayoutConstraint.activate([
            
            scrollView.topAnchor.constraint(equalTo: safeG.topAnchor),])
        
        for i in 1...8 {
            let tf = UITextField()
            tf.borderStyle = .roundedRect
            tf.text = "\(i)"
            stack.addArrangedSubview(tf)
            if i == 1 {
                firstTextField = tf
            }
        }
        
        register()

    }

    func register() {
        NotificationCenter.default.addObserver(self,selector: #selector(keyboardWillShow(notification:)),name: UIResponder.keyboardWillShowNotification,object: nil)
        NotificationCenter.default.addObserver(self,selector: #selector(keyboardWillHide(notification:)),name: UIResponder.keyboardWillHideNotification,object: nil)
    }
    
    @objc func keyboardWillShow(notification: NSNotification) {
        let keyboardFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)
        let keyboardSize = keyboardFrame?.size
        print("keyboardWillShow " + String(describing: keyboardSize))

        // only change scroll view insets if .bottom != keyboard height
        if scrollView.contentInset.bottom != keyboardSize?.height {
            print("setting insets")
            let contentInsets = UIEdgeInsets(top: 0.0,left: 0.0,bottom: keyboardSize!.height,right: 0.0)
            scrollView.contentInset = contentInsets
            scrollView.scrollIndicatorInsets = contentInsets
        }
    }
    
    @objc func keyboardWillHide(notification: NSNotification) {
        print("keyboardWillHide")
        
        let contentInsets = UIEdgeInsets(top: 0.0,bottom: 0.0,right: 0.0)
        scrollView.contentInset = contentInsets
        scrollView.scrollIndicatorInsets = contentInsets
    }

}

我们会看到相同的(错误的)行为......我们会看到“double will show”通知被触发。我添加了一个 if 子句,所以我们只在键盘框架发生变化时才更改内容插入,因此我们排除了罪魁祸首。

同样,只要我们在任何文本字段中输入一个字符,预期行为就会恢复正常——AND 我们不再收到“double will show”通知。

不幸的是,在尝试了几种不同的方法来强制之后,我还没有找到解决方法:(

,

好的,谢谢大家的关注。

为了尽量减少这种行为,我们解决的是设置滚动视图以在拖动时关闭 KB。这会将“双重呼叫”移动到键盘隐藏而不是显示时。