At the current evening of the launch, the test found that there was no response when a button was clicked. The reappearance path given by the test was as follows: in the Release package, when entering the corresponding interface, the keyboard pop-up button could be clicked normally, while the keyboard retreat button could not be clicked, and the DEBUG package could also be clicked normally. If you are interested in this bug, click here to download the simplest demo.

The core code that can be reproduced is as follows

class ViewController: UIViewController {
    var btn: UIButton = {
        let btn = UIButton(type: .system)
        btn.frame = CGRect(x: 100, y: 100, width: 60, height: 40)
        btn.setTitle("点我", for: .normal)
        btn.addTarget(self, action: #selector(onTap), for: .touchUpInside)
        return btn
    }()
    
    var textField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Please enter"
        textField.frame = CGRect(x: 100, y: 160, width: 60, height: 40)
        return textField
    }()
    
    override func viewDidLoad(a) {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.white
        view.addSubview(btn)
        view.addSubview(textField)
    }

    @objc func onTap(a) {
        print("onTap")}}extension UIWindow {
    #if DEBUG
    #else
    open override var canBecomeFirstResponder: Bool {
        return true
    }
    #endif
}
Copy the code

Before looking at the explanation below, you can take a look at the code above. Should it reproduce the above bug?

The answer, of course, must be that the above question will definitely arise. Of course you might have the following question, right?

  1. Why is the onTap method called during debug?
  2. Why is the onTap method called when the keyboard is up?

In the above code, the BTN was intended to be lazy-loaded, but a copy error resulted in one less lazy being copied, causing the BTN to become a non-lazy-loaded stored variable, followed by an immediately executed closure. The ViewController hasn’t been initialized yet when the closure executes, and the self in the closure is really a Function, and since the target of BTN is going to try to convert to NSObject, it’s going to fail, so we’re simply going to say that the target of BTN is equal to nil.

That is, the above code can simply be equivalent to btn.addTarget(nil, action: #selector(onTap), for:.touchupinside).

And when you add an event to a button, if target is nil, the system does something like this

  1. First get the current APP’s firstResponder, if firstResponder is nil do step 4, otherwise do the next step
  2. Determine if firstResponder implements the action for this target, and if so, call.
  3. If not, the nextResponder of the firstResponder is realized, and so on until the nextResponder is nil
  4. Determine whether the BTN implements the corresponding action, if so, then call.
  5. Otherwise, it will judge whether the nextResponder of BTN has realized the corresponding action, and so on, until the nextResponder is nil

For more information about the addTarget method of UIButton, see the addTarget method exploration of UIButton

Back to the code above:

So when you debug, you go to this page, firstResponder equals nil, you go to step 4, and BTN obviously doesn’t implement onTap, so it looks for its nextResponder, which is the VC View, and it doesn’t implement onTap, But the VC View’s nextResponder is the ViewController, which implements the onTap method, so onTap is going to be called.
So when we release, we go to this page firstResponder is equal to keyWindow, and we’re going to do nothing. (The above extension adds an extension to UIWindow to shake it, but only adds it to release, because our project is connected to RN. RN hooks system events in the debug, so we do some extra processing in our project, which is not very important.)
So when we release, when we go to this page and we pop up the keyboard, the firstResponder is equal to the textField, it takes the first step, the nextResponder of the textField the nextResponder of the textField is VC, So the onTap method of the ViewController is also going to be called