Swift 中的 Selector

由于種種原因,簡書等第三方平臺博客不再保證能夠同步更新,歡迎移步 GitHub:https://github.com/kingcos/Perspective/。謝謝!

Selectors in Swift

  • Info:
  • Swift 3.0
  • Xcode 8.2.1
  • macOS 10.12.4 beta (16E144f)

前言

今天是大年初四(捂臉:提筆的時候是初一),總算過農歷新年了,總算可以歇一歇了。越來越感慨時間過得飛快,計劃總是趕不上變化。寒假倒計時 20 天,卻有很多事都還沒有完成。。

常用純代碼來開發的同學都應該比較熟悉這個方法:

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

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容