iOS 简单实现KVO(Swift版)

以下部分内容源于喵神的Swifter-KVO章节,大家也可以去看看,收获很大。本文只作用于自己收集和整理便于学习和复习所用。

一:什么是KVO,它拿来干什么的?

KVO(Key-Value Observing)顾名思义就是通过键(key)值(value),来对属性进行观察。
通过观察可以实现hook属性值,以及监听属性变化,并通知订阅者。这样一个强大的属性可以轻松实现松耦合的结构,使代码更加灵活和强大,例如双向绑定,通过监听model的值来刷新UI。

二:KVO原理

KVO是通过isa-swizzling技术实现的。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

当然上面的一堆原理如果不实际进行操作体验是很难理解的。所以简单来说就是KVO基于OC的动态派发特点和KVC为基础,这些都是依赖于OC的运行时(runtime)概念。而在swift中,默认是禁用了动态派发的。所以在swift中实现KVO,就需要额外的操作了。

三:实现KVO

那么如何实现其OC的动态派发特性呢?

  • 1.首先Class需继承自NSObject
  • 2.属性前加上@objc dynamic

如下:

1
2
3
class Preson: NSObject {
@objc dynamic var age = 0
}

其实在Swift3中只需要加上dynamic就可以了,而Swift4以后则还需要@objc

因为必须使用@objc属性标记Swift类或协议才能在Objective-C中访问和使用。该属性告诉编译器可以从Objective-C访问这段Swift代码。

接下来就可以对属性age实现KVO了

  • 1.实例化刚才的Preson对象,并对其age属性添加监听

    1
    2
    3
    4
    5
    6
    //创建对象
    let p = Preson()
    //为p对象的age属性设置监听。
    //options为枚举数组,传入了new和old两个值,表示我们需要监听改变前和改变后的值。
    //context是unsafePointer类型,表示不安全的指针类型(因为在Swift手动操作指针,修改内存是一件非常不安全且不考靠的行为),可以传入一个指针地址。
    p.addObserver(self, forKeyPath: "age", options: [.new, .old], context: nil)
  • 2.重写observeValue方法,当age属性发生改变时得到通知。

    1
    2
    3
    4
    5
    6
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "age" {
    print("age被改变了")
    print(change as Any)
    }
    }
  • 3.实现一个屏幕点击事件,当点击屏幕后,修改age值 = 20

    1
    2
    3
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    p.age = 20
    }
  • 4.当点击屏幕后,可以看到打印出了修改前和修改后的数据,old值 = 0,new值 = 20

  • 5.最后记得还要注销KVO,可以在页面的deinit方法中,也可以是手动注销。

    1
    2
    3
    deinit { 
    p.removeObserver(self, forKeyPath: "age")
    }
四:以下是完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import UIKit

class Preson: NSObject {
@objc dynamic var age = 0
}

class ViewController: UIViewController {

//1.创建对象
let p = Preson()

override func viewDidLoad() {
super.viewDidLoad()
//为p对象的age属性设置监听。
//options为枚举数组,传入了new和old两个值,表示我们需要监听改变前和改变后的值。
//context是Any类型,可以传入任意类型数据,方便在监听到变化时获取该数据。
p.addObserver(self, forKeyPath: "age", options: [.new, .old], context: nil)
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "age" {
print("age被改变了")
print(change as Any)
}
}

deinit {
p.removeObserver(self, forKeyPath: "age")
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
p.age = 20
}

}