第一章 神奇
猜猜哪个btnTapped函数会被调用?
import UIKit
class DemoViewController : UIViewController {
override func viewDidLoad() {
let view = DemoView()
let btn = view.btn;
self.view.addSubview(btn!)
}
@objc func btnTapped(_ sender: UIButton) {
print("print: surprise!")
}
}
class DemoView {
var btn : UIButton!
init() {
btn = UIButton()
btn.frame = CGRect(x: 110, y: 70, width: 100, height: 44)
btn.backgroundColor = UIColor.blue
btn.setTitle("Press me", for: .normal)
btn.setTitle("I'm Pressed", for: .highlighted)
btn.addTarget(self, action: #selector(DemoView.btnTapped(_:)), for: .touchUpInside)
}
@objc func btnTapped(_ sender: UIButton) {
print("print: demo button is tapped")
}
}
答案是:surprise!
第二章 解惑
正确的写法应该用singleton模式来实现DemoView,如下。
import UIKit
class DemoViewController : UIViewController {
override func viewDidLoad() {
let view = DemoView._instance
let btn = view.btn;
self.view.addSubview(btn!)
}
@objc func btnTapped(_ sender: UIButton) {
print("print: surprise!")
}
}
class DemoView {
var btn : UIButton!
static let _instance = DemoView()
private init() {
btn = UIButton()
btn.frame = CGRect(x: 110, y: 70, width: 100, height: 44)
btn.backgroundColor = UIColor.blue
btn.setTitle("Press me", for: .normal)
btn.setTitle("I'm Pressed", for: .highlighted)
btn.addTarget(self, action: #selector(DemoView.btnTapped), for: .touchUpInside)
}
@objc func btnTapped(_ sender: UIButton) {
print("print: demo button is tapped")
}
}
<b>洞察:</b>
- Selector是runtime时延迟动态绑定,#selector糖里面只是为了compiler帮助检查prototype正确性(对以前的字符串写法”btnTapped:”无法在编译期检查问题的优化),所以即使写成DemoView.btnTapped,也只是告诉compiler这个原型参考函数,而并非Selector在runtime时真正选取的函数。
因为函数选取是在Obj-C名字空间里做,所以需要用@objc修饰需要暴露给Obj-C的函数。 - addTarget的第一个参数是Obj-C消息机制的receiver对象,把selector选取的函数发送给对象,其实就是“对象的方法调用”。
- self也是运行时选取,所以才会出现神奇的现象,即调用了DemoViewController而不是DemoView的btnTapped函数。
也因此,如果把上面错误写法中的btn.addTarget(self改为btn.addTarget(DemoView.self,即强行要求调用DemoView对象(instance of the class)的函数,运行时点击按钮就会得到错误,unrecognized selector +[DemoView btnTapped:]。 - BTW,可以看到,因为swift的lazy initialization特性,所以实现singleton异常简单。
第三章 祛魅
Debug容易成为一门玄学。以下是国内外网友分享的一些“迷信“。
-
#selector选择的类必须继承NSObject,也就是要写成class DemoView : NSObject
实测:假。 -
原型要写成btnTapped(_:)。
实测:假。
第四章 优雅
通过extension Selector可以写的更加优雅。
import UIKit
class DemoViewController : UIViewController {
override func viewDidLoad() {
let view = DemoView._instance
let btn = view.btn;
self.view.addSubview(btn!)
}
@objc func btnTapped(_ sender: UIButton) {
print("print: surprise!")
}
}
class DemoView {
var btn : UIButton!
static let _instance = DemoView()
private init() {
btn = UIButton()
btn.frame = CGRect(x: 110, y: 70, width: 100, height: 44)
btn.backgroundColor = UIColor.blue
btn.setTitle("Press me", for: .normal)
btn.setTitle("I'm Pressed", for: .highlighted)
btn.addTarget(self, action: .btnTapped, for: .touchUpInside)
}
@objc func btnTapped(_ sender: UIButton) {
print("print: demo button is tapped")
}
}
private extension Selector {
static let btnTapped = #selector(DemoView.btnTapped)
}
<b>解读:</b>
action:后面直接用点号的写法是一种糖,compiler看起来就是Selector.btnTapped,也正是最下方的extension代码所实现的。
From painful to painless.
网友评论