【iOS】Swift系列三十 - 从OC到Swift(二)

协议、关联对象、KVO等Swift和OC的关系。

一、协议

1.1. 只能被class继承的协议

示例代码:

1
2
3
protocol Runnable1: AnyObject { }
protocol Runnable2: class { }
@objc protocol Runnable3 { }

@objc修饰的协议,还可以暴露给OC去遵守实现。

1.2. 可选协议

正常情况下,Swift定义的协议内容都需要实现,如果需要可选实现,可以定义一个协议扩展,在扩展中空实现需要可选实现的协议。

也可以通过@objc定义可选协议,这种协议只能被class遵守。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@objc protocol Runnable {
@objc optional func run1()
func run2()
func run3()
}

class Dog: Runnable {
func run2() {
print("Dog run2")
}
func run3() {
print("Dog run3")
}
}
var d = Dog()
d.run2() // 输出:Dog run2
d.run3() // 输出:Dog run3

加上optional就必须加@objc。并且只能被类实现协议。

二、dynamic

@objc、dynamic修饰的内容会具有动态性,比如调用方法会走runtime那一套流程。

示例代码:

1
2
3
4
5
6
7
class Dog: NSObject {
@objc dynamic func test1() { }
func test2() { }
}
var d = Dog()
d.test1()
d.test2()

test1汇编(消息转发):

test2汇编(虚表):

三、KVC/KVO

Swift支持KVC/KVO的条件:

  • 属性所在的类、监听器最终继承自NSObject(因为OC的KVC/KVO走的是runtime,而使用runtime必然会用isaisa又是NSObject的)
  • @objc dynamic修饰对应的属性

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Observer: NSObject {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("observeValue", change?[.newKey] as Any)
}
}

class Person: NSObject {
@objc dynamic var age: Int = 0
var observer: Observer = Observer()
override init() {
super.init()
self.addObserver(observer, forKeyPath: "age", options: .new, context: nil)
}
deinit {
self.removeObserver(observer, forKeyPath: "age")
}
}
var p = Person()
p.age = 20 // 输出:observeValue Optional(20)
p.setValue(30, forKey: "age") // 输出:observeValue Optional(30)

如果觉得上面的代码还需要初始化观察者对象比较麻烦,还可以使用block方式的KVO。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person: NSObject {
@objc dynamic var age: Int = 0
var observation: NSKeyValueObservation?
override init() {
super.init()
observation = observe(\Person.age, options: .new) {
(person, change) in
print("Block", change.newValue as Any)
}
}
}
var p = Person()
p.age = 20 // 输出:Block Optional(20)
p.setValue(30, forKey: "age") // 输出:Block Optional(30)

注意监听的属性前面要加上斜杠\

四、关联对象

在Swift中,class依然可以使用关联对象。

默认情况下,extension不可以增加存储属性。借助关联对象,可以实现类似extensionclass增加存储属性的效果。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
class Person { }
extension Person {
private static var AGE_KEY: Void?
var age: Int {
get {
(objc_getAssociatedObject(self, &Self.AGE_KEY) as? Int ) ?? 0
}
set {
objc_setAssociatedObject(self, &Self.AGE_KEY, newValue, .OBJC_ASSOCIATION_ASSIGN)
}
}
}

关联对象的本质就是键值对,但是需要我们自己绑定一个编译期已知地址值(静态存储属性)。由于仅需要地址绑定,所以为了内存空间考虑,静态存储属性使用Bool类型或者Void?类型最好(仅占用1个字节)。

五、资源名管理

在项目开发中,经常有可能会遇到一张图片好多地方在使用(图片名为标识),一个标题很多地方使用等等。其实我们可以把这些相同的资源统一起来做一个标识符,防止后期多处修改。

示例代码一:

1
2
3
4
5
let img = UIImage(named: "logo")
let btn = UIButton(type: .custom)
btn.setTitle("按钮", for: .normal)

performSegue(withIdentifier: "login_main", sender: self)

如上面示例代码,如果很多地方都用到了名字为logo的图片,描述为按钮的文字等,一旦遇到修改,简直是地狱一般(虽然可以全局修改,但是有可能会一键修改到工程死翘翘)。

遇到这种资源名管理,我们可以有多种处理方式。下面介绍下参考Android的资源名管理方式:

示例代码二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum R {
enum string: String {
case add = "按钮"
}
enum image: String {
case logo
}
enum segue: String {
case login_main
}
}
let img = UIImage(named: R.image.logo)
let btn = UIButton(type: .custom)
btn.setTitle(R.string.add, for: .normal)

performSegue(withIdentifier: R.segue.login_main, sender: self)

R代表Resource,Swift仿Android的写法,Swift主要利用了枚举的关联值

示例代码三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 原始
let img = UIImage(named: "logo")
let font = UIFont(name: "Arial", size: 17)

// 封装资源管理
enum R {
enum image {
static var logo = UIImage(named: "logo")
}
enum font {
static func arial(_ size: CGFloat) -> UIFont? {
UIFont(name: "Arial", size: size)
}
}
}

// 使用资源管理
let img = R.image.logo
let font = R.font.arial(15)

上面对image的封装有两点考量:

  1. 可以直接通过名称返回一个Image对象。

  2. 静态属性在内存中只有一份,后面任何地方再次用到时可直接从内存中加载数据(除非需要每次都加载新数据)。

更多优秀的思路参考: