swift那神奇的#selector

第一章 神奇

猜猜哪个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")
    }
    
}

洞察:

  • 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)
}

解读:
action:后面直接用点号的写法是一种糖,compiler看起来就是Selector.btnTapped,也正是最下方的extension代码所实现的。

From painful to painless.

你可能感兴趣的:(swift那神奇的#selector)