. 不够优雅的 #selector
Swift 中的 Selector 就是 Objective-C 中的 SEL, 在 SWift 中重新封装成结构体, 并扩展了一些方法也同样提供了使用字符串的进行初始化
- // 官方定义
- public struct Selector : ExpressibleByStringLiteral {
- /// Create a selector from a string.
- public init(_ str: String)
- /// Create an instance initialized to `value`.
- public init(stringLiteral value: String)
- }
但是出于字符串操作的不安全性, 封装了类似宏类型的语法糖 #selector, 允许使用编辑器的自动补全功能, 杜绝了直接使用字符串导致错误的可能, 同时还会检查当前的方法是否支持动态派发
但是每次使用 #selector 都感觉不顺手, 介于 #selector 不够优雅, 我们可以对其进行优化
一. 使用 Struct 进行封装
- // 定义
- struct Action {
- static let tapped: Selector = #selector(SomeClass.tap(_:))
- }
- // 使用
- button.addTarget(self, action: Action.tapped, for: .touchUpInside)
OK, 使用结构体的方式, 我们终于可以在实际使用的地方避免了使用 #selector, 但是每次都定义一个 Action 吗? 这样不免会让人感觉很累赘
二. 使用 Extention 进行封装
- // 定义
- extension Selector {
- static let tapped: Selector = #selector(SomeClass.tap(_:))
- }
- // 使用
- button.addTarget(self, action: .tapped, for: .touchUpInside)
好了, 使用扩展的方式, 让我们可以直接定义自己的选择器, 同时使用类型推断可以省略部分代码, 但是其他文件也想使用 tapped 时有问题:
多文件同时使用将导致静态成员混乱, 命名被占用
那么我们为扩展添加权限, 让不同文件可以使用相同的命名:
- // A.swift
- fileprivate extension Selector {
- static let tapped: Selector = #selector(AClass.tap(_:))
- }
- // B.swift
- fileprivate extension Selector {
- static let tapped: Selector = #selector(BClass.tap(_:))
- }
修改后, 实际使用还是有很多问题:
每个方法需要定义一个静态成员, 使用麻烦
不同类的相同方法名需要分别定义静态成员, 不可重用
三. 使用 Protocol 进行封装
为了解决上面的问题, 尝试使用 Swift 中最强大的协议进行解决, 毕竟 Swift POP
- // 定义
- protocol Selectorable {
- var tapAction: Selector { get }
- func tap(_ sender: UIButton)
- }
- // 实现
- class Person: Selectorable {
- var tapAction: Selector {
- return #selector(tap(_:))
- }
- func tap(_ sender: UIButton) {
- print("that's all right")
- }
- }
使用协议后, 同时解决了上面的几个问题, 文件间不会造成冲突也不需要每个文件都去重新定义静态成员 但是每次都实现属性和方法稍显累赘, 是否可以进行优化, 只在需要的时候重写?
使用 Protocol Extension 来提供默认的实现:
- // 添加默认实现
- extension Selectorable {
- var tapAction: Selector {
- return #selector(Self.tap(_:))
- }
- }
可添加实现后, 会出现 error:
Argument of '#selector' refers to instance method 'tapButton' that is not exposed to Objective-C
(必须要将该方法暴露给 Objective-C runtime)
那么我们为该方法添加 @objc 修饰符, 同时因为协议内包含有 @objc 的方法, 则协议本身必须是 @objc, 也就是整个协议注册到 Objective-C runtime 使用动态派发 那么, 问题来了, 在 Swift 中协议的扩展只可以采用静态派发 [1], 所以我们在协议扩展内的默认实现也是静态派发的, 不满足整个协议动态的要求 (编写时也会出现预处理错误), 那么一切又回到了原点, 我们需要实现协议内所有的属性和方法
但是, 你可还记得 Selector 提供了字符串的初始化方法, 这样我们就可以使用字符串初始化 [2] Selector 来逃过编译器的语法检查:
- // 添加默认实现
- extension Selectorable {
- var tapAction: Selector {
- return Selector("tap:")
- }
- }
OK, 这样明目张胆逃过编译器的语法检查, 就出现了 warning:
Use '#selector' instead of explicitly constructing a 'Selector'
(建议使用 #selector 来避免显式地调用初始化方法), 但是这就是我们的目的由于 Swift 并没有实现预处理器, 所以 Objective-C 内的 #pragma[3] 在这里并不能使用.
警告先不管, 现在看起来就可以比较方便地使用了:
- // 定义
- protocol Selectorable {
- var tapAction: Selector { get }
- func tap(_ sender: UIButton)
- }
- extension Selectorable {
- var tapAction: Selector {
- return Selector("tap:")
- }
- }
- // 实现
- class Person: Selectorable {
- func tap(_ sender: UIButton) {
- print("that's all right")
- }
- }
我们可以将项目中比较常用的一些 Selector 抽象到协议内, 如自定义的返回, 编辑操作:
- // 定义
- protocol Selectorable {
- var tapAction: Selector { get }
- func tap(_ sender: UIButton)
- var backAction: Selector { get }
- func back(_ sender: UIButton)
- var editAction: Selector { get }
- func edit(_ sender: UIButton)
- }
- extension Selectorable {
- var tapAction: Selector { return Selector("tap:") }
- var backAction: Selector { return Selector("back:") }
- var editAction: Selector { return Selector("edit:") }
- }
- // 实现
- class Person: Selectorable {
- func tap(_ sender: UIButton) {}
- func back(_ sender: UIButton) {}
- func edit(_ sender: UIButton) {}
- }
看起来不错, 但是, 你也应该发现了, 不管我们需不需要这个方法, 我们都必须实现协议内的所有方法, 但是如果为协议的方法添加修饰符 optional, 那么势必将整个 Protocol 标志为 @objc, 那么又回到之前提到的问题: 协议扩展内的默认实现将失效
那么换一种思路, 在协议扩展内实现所有的默认实现 (但是实现将不会被调用 [4]):
- // 扩展实现
- extension Selectorable {
- var tapAction: Selector { return Selector("tap:") }
- var backAction: Selector { return Selector("back:") }
- var editAction: Selector { return Selector("edit:") }
- /*this is just a placeholder, and the function will never be invoked \
- because of static dispatch*/
- func tap(_ sender: UIButton) {}
- func back(_ sender: UIButton) {}
- func edit(_ sender: UIButton) {}
- }
- class Person: Selectorable {
- // 默认不需要实现任何方法, 按需实现对应的方法即可
- }
一切 OK, 接下来在实际项目中运行, 抛出了我们熟悉的运行时错误:
unrecognized selector send to instance 0x00000123456
为什么抛出该错误:
因为没有实现该方法, 你肯定在 Objective-C 中遇见过
那么实现了协议中的方法, 运行, 可还是抛出相同错误!
因为实现了该方法, 但是没有注册到运行时
为什么还要注册到运行时呢? 是否还记得我们之前逃过编译器语法检查的事? 大家肯定知道, Target-Action 使用的就是 Objective-C Runtime 的消息机制, 而类中对于协议的实现默认是使用函数表派发 (Virtual Dispatch) 的, 这就导致 Selector 找不到对应的函数实现, 那么我们可以使用 @objc 将方法注册到 Runtime 解决该错误
完成后运行, ALL RIGHT!
当然, 也还有一些问题, 暂时没有找到解决思路
由于 Swift 不支持预处理器, 我们不能在代码中添加指令去忽略这个警告, 但是可以在 Building Setting 内搜索 warning 来解除警告.
有时候我们需要很多很多的 Selector 用来处理各种不同的按钮事件, 通知事件, 有一些只需要调用一次, 如果把只需要调用一次的事件写入协议, 将会造成协议庞大难以维护 如果不写入协议, 那么将会造成同一份代码内有两种方式书写 selector 调用, 一种是原始的 #selector, 另一种是协议的方式
逃过编译器检查, 势必会造成一些编写造成的错误, 例如忘记在遵循的协议方法前添加 @objc
Protocol 默认使用 Virtual Dispatch, 使用数组保存函数, 扩展内的函数不能添加到表尾
可自行查阅 Swift 中的方法转换成 Objective-C 方法的规则
Clang Warning 查阅表 http://fuckingclangwarnings.com
当遵循协议的类没有实现方法, 使用 Target-Action 调用协议默认实现时, 由于协议默认实现为静态派发, 不会注册到 Runtime, 所以 Selector 获取不到默认实现的函数, 抛出错误
来源: http://www.jianshu.com/p/a56776a578fe