Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift 中的 Selector #24

Open
kingcos opened this issue Sep 1, 2018 · 0 comments
Open

Swift 中的 Selector #24

kingcos opened this issue Sep 1, 2018 · 0 comments
Assignees
Labels
[Focus] 即专注

Comments

@kingcos
Copy link
Owner

kingcos commented Sep 1, 2018

Date Notes Swift Xcode
2017-01-31 首次发布简书 3.0 8.2.1

What

常用纯代码来开发的同学都应该比较熟悉这个方法:

func addTarget(_ target: Any?, action: Selector, for controlEvents: UIControlEvents)

Selector 源自 Objective-C,例如 SEL 类型,以及 @selector() 方法选择器。Swift 中也兼容了这个概念,不过随着 Swift 的迭代,Selector 的一些写法也出现了很大的变化。比较遗憾的是,官方文档对于 Selector 没有介绍。

Selector in Xcode Documentation & API Reference

因此只能自己总结一下 Swift 3.0 中的 Selector,便有利于自己理解,也便于以后的参考。注:以下 Demo 中的 cyanButton 是用 StoryBoard 拖拽的。

Selector 类型

Swift 中的 Selector 类型其实就是 Objective-C 中的 SEL 类型。在 Swift 中,Selector 的本质是结构体。常用的构造 Selector 类型变量的方法有以下几种:

  • public init(_ str: String)

类似 Objective-C 中的 NSSelectorFromString,Swift 中的 Selector 也可以使用字符串来构造:

@IBOutlet weak var cyanButton: UIButton!

override func viewDidLoad() {
    super.viewDidLoad()

    cyanButton.addTarget(self,
                         action: Selector("cyanButtonClick"),
                         for: .touchUpInside)
}

func cyanButtonClick() {
    print(#function)
}
  • #selector()

通过字符串构造 Selector 变量是一种方法,但是当在上例中 Xcode 会提示这样的警告:「Use '#selector' instead of explicitly constructing a 'Selector'」。即使用 #selector() 代替字符串明确构造 Selector。

@IBOutlet weak var cyanButton: UIButton!

override func viewDidLoad() {
    super.viewDidLoad()

    cyanButton.addTarget(self,
                         action: #selector(ViewController.cyanButtonClick),
                         for: .touchUpInside)
}

func cyanButtonClick() {
    print(#function)
}

#selector() 的好处是不再需要使用字符串来构造。因为当使用字符串构造时,若传入的字符串没有对应的方法名,那么程序在执行时就会直接崩溃:「unrecognized selector sent to instance」。

若当前作用域构造 Selector 的方法名唯一时,可以直接使用方法名,而省略作用域。

cyanButton.addTarget(self,
                     action: #selector(cyanButtonClick),
                     for: .touchUpInside)

若是 Swift 中的私有方法,则必须赋予其 Objective-C 的 runtime(运行时)。即在方法名前加上 @objc

@IBOutlet weak var cyanButton: UIButton!
@IBOutlet weak var anotherCyanButton: UIButton!

override func viewDidLoad() {
    super.viewDidLoad()
    cyanButton.addTarget(self,
                         action: #selector(ViewController.cyanButtonClick(_:)),
                         for: .touchUpInside)

    // 当前作用域 cyanButtonClick 存在冲突,不能直接使用方法名
    //「Ambiguous use of 'cyanButtonClick'」
    // anotherCyanButton.addTarget(self,
                                action: #selector(cyanButtonClick),
                                for: .touchUpInside)
}

// 无参方法
func cyanButtonClick() {
    print(#function)
}

// 有参私有方法
@objc private func cyanButtonClick(_ button: UIButton) {
    let btnLabel = button.titleLabel?.text ?? "nil"
    print(btnLabel)
    print(#function)
}

当遇到上述存在歧义的相同方法名时,也可以使用强制类型转换来解决:

@IBOutlet weak var cyanButton: UIButton!
@IBOutlet weak var anotherCyanButton: UIButton!

override func viewDidLoad() {
    super.viewDidLoad()

    let methodA = #selector(cyanButtonClick as () -> ())
    let methodB = #selector(cyanButtonClick as (UIButton) -> ())

    cyanButton.addTarget(self,
                         action: methodA,
                         for: .touchUpInside)
    anotherCyanButton.addTarget(self,
                                action: methodB,
                                for: .touchUpInside)
}

func cyanButtonClick() {
    print(#function)
}

@objc private func cyanButtonClick(_ button: UIButton) {
    let btnLabel = button.titleLabel?.text ?? "nil"
    print(btnLabel)
    print(#function)
}
  • #selector() & Seletcor("")

通过上面的 Demo,也可以看出 #selector() 更加安全、清晰,但是 Seletcor("") 并不是一无是处。当我们需要调用标准库中的私有方法时,只能通过字符串来构造。

为了方便测试,此处自定义了一个 CustomViewController。其中带有私有方法:@objc private func privateFunc() 以及 func defaultFunc()。此处使用的 ViewController 继承自 CustomViewController

CustomViewController.swift

class CustomViewController: UIViewController {

    @objc private func privateFunc() {
        print(#function)
    }

    func defaultFunc() {
        print(#function)
    }

}

ViewController.swift

class ViewController: CustomViewController {

    @IBOutlet weak var cyanButton: UIButton!
    @IBOutlet weak var anotherCyanButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        cyanButton.addTarget(self,
                             action: #selector(defaultFunc),
                             for: .touchUpInside)
        anotherCyanButton.addTarget(self,
                                    action: Selector("privateFunc"),
                                    for: .touchUpInside)
    }

}

因为父类的私有方法对子类来说是不可见的,直接使用 #selector() 无法通过编译,但这个方法确实存在,所以这里只能使用字符串来构造 Selector。

当然这里 Xcode 会提示警告,但仍然可以编译通过并运行,所以这并不是官方提倡的行为。这是我在将系统边缘返回改写全屏返回时,发现私有的 handleNavigationTransition: 方法不能通过 #selector(),因此使用了字符串代替。

Syntax Sugar

配合 Swift 的 Extension,可以使用其管理当前控制器的所有 Selector:

import UIKit

fileprivate extension Selector {
    static let redButtonClick = #selector(ViewController.redButtonClick(_:))
    static let cyanButtonClick = #selector(ViewController.cyanButtonClick)
}

class ViewController: CustomViewController {

    @IBOutlet weak var cyanButton: UIButton!
    @IBOutlet weak var redButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        cyanButton.addTarget(self,
                             action: .cyanButtonClick,
                             for: .touchUpInside)
        redButton.addTarget(self,
                                    action: .redButtonClick,
                                    for: .touchUpInside)
    }

    func cyanButtonClick() {
        print(#function)
    }

    func redButtonClick(_ button: UIButton) {
        let btnLabel = button.titleLabel?.text ?? "nil"
        print(btnLabel)
        print(#function)
    }

}

getter & setter

Swift 3.0 中加入了 Selector 引用变量(不可为常量)的 getter 和 setter 方法:

class Person: NSObject {
    dynamic var firstName: String
    dynamic let lastName: String
    dynamic var fullName: String {
        return "\(firstName) \(lastName)"
    }

    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
}


fileprivate extension Selector {
    static let firstNameGetter = #selector(getter: Person.firstName)
    static let firstNameSetter = #selector(setter: Person.firstName)
}

Reference

@kingcos kingcos self-assigned this Sep 1, 2018
@kingcos kingcos added the [Focus] 即专注 label Sep 1, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Focus] 即专注
Projects
None yet
Development

No branches or pull requests

1 participant