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

806 阅读4分钟

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

一、协议

1.1. 只能被class继承的协议

示例代码:

protocol Runnable1: AnyObject { }
protocol Runnable2: class { }
@objc protocol Runnable3 { }

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

1.2. 可选协议

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

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

示例代码:

@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那一套流程。

示例代码:

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修饰对应的属性

示例代码:

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。

示例代码:

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增加存储属性的效果。

示例代码:

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个字节)。

五、资源名管理

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

示例代码一:

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

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

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

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

示例代码二:

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主要利用了枚举的关联值

示例代码三:

// 原始
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. 静态属性在内存中只有一份,后面任何地方再次用到时可直接从内存中加载数据(除非需要每次都加载新数据)。

更多优秀的思路参考:

更多系列文章,请关注 微信公众号【1024星球】