iOS UIButton 偶现不响应问题
一. 问题背景
有同时反馈,评价弹框的匿名评价按钮,在他的手机上点击没有任何响应,但是在其他人的手机上面是正常的,因此进行排查。
二. 问题分析
经排查项目里面的代码对按钮的声明类似如下:
var testButton: UIButton = {
let btn = UIButton()
btn.setTitle("点击", for: .normal)
btn.setTitleColor(.red, for: .normal)
btn.addTarget(self, action: #selector(testButtonClicked), for: .touchUpInside)
return btn
}()
swfit
声明UIButton
变量,没有加上lazy
,使得这个变量调用是在init
构造方法之前,所以这里testButton
的target
的self
,因为还没调用init
方法,所以这里self
是nil
。
之所以nil
也没问题,是因为点击触摸到屏幕的时候,首先通过响应链找到第一响应者testButton
,然后testButton
将添加到自身点击事件发送给UIApplication
,UIApplication
判断如果target
为nil
,就会以第一响应者testButton
为起点,沿着响应链去判断,响应链条上面的视图是否可以响应点击事件,如果可以响应,则将事件发送给该视图,触发selector
,停止传递;
如果响应链上的视图都不能响应该事件,就直接抛弃这个响应事件,因此这里即使target
传入了nil
,沿着响应链去寻找方法响应者,刚好找到了FJFButtonTestView
可以响应#selector(testButtonClicked)
,所以直接将事件传给FJFButtonTestView
的去处理。
但在有问题的手机上,调试发现,因为testButton
是写在FJFButtonTestView
里面的,testButton
响应的selector
方法也是放在FJFButtonTestView
里面,因为传入的target
为nil
,UIApplication
则直接将事件发送到UIViewController
上,直接沿着UIViewController
的响应链去传递,而不是从第一响应者的响应链开始传递,因此导致了,放到FJFButtonTestView
上的点击事件压根不响应。
至于为什么在有问题手机系统上面,没有第一响应者testButton
为起点,沿着响应链去查找是否可以响应该事件的响应者,而是直接从UIViewController
开始查找,这个问题,我看官方文档是这样说的:
When adding an action method to a control, you specify both the action method and an object that defines that method to the addTarget(_:action:for:) method. (You can also configure the target and action of a control in Interface Builder.) The target object can be any object, but it’s typically the view controller’s root view that contains the control. If you specify nil for the target object, the control searches the responder chain for an object that defines the specified action method.
https://developer.apple.com/documentation/uikit/uicontrol
这里意思是说如果你的target
传入为nil
,那么UIControl
将在响应链中搜索定义action
的对象。文章并没有明确是从第一响应者沿着响应链去查找。
这是我当时拿到反馈问题手机调试的堆栈(是另一个OC
版本的Demo):
从堆栈可以很明显看出UIApplication
直接将事件发送给FJFButtonClickedViewController
上定义的- (void)testButtonClicked:(UIButton *)sender
响应事件。
至于本质原因我也没找到,如果有了解的朋友,还请告知下。
三. 解决方案
方案一: 添加lazy
声明为懒加载方式
// MARK: - Lazy
lazy var testButton: UIButton = {
let btn = UIButton()
btn.setTitle("点击", for: .normal)
btn.setTitleColor(.red, for: .normal)
btn.addTarget(self, action: #selector(testButtonClicked), for: .touchUpInside)
return btn
}()
通过将变量声明为lazy
,懒加载方式,等到View
初始化完成之后,再去真正的调用。
方案二:将button声明为可选项,等到真正用到再去初始化
private var testButton: UIButton?
// MARK: - Life
override init(frame: CGRect) {
super.init(frame: frame)
setupViewControls()
layoutViewControls()
}
// MARK: - Private
private func setupViewControls() {
let btn = UIButton()
btn.setTitle("点击", for: .normal)
btn.setTitleColor(.red, for: .normal)
btn.addTarget(self, action: #selector(testButtonClicked), for: .touchUpInside)
self.addSubview(btn)
testButton = btn
}
这两种方式都能保证,button
添加target
的self
是有值的。