KVO,即Key-value observation,是苹果提供的一种机制,它可以使监听对象在被监听对象的数值发生改变时收到通知,进而去进行响应的处理。
KVO实现起来比较简单,主要的流程只有3个:
- 添加观察者
- 在监听方法中处理监听结果
- 监听结束后移除观察者
下面我们用一个实际的例子来说明一下这3个步骤。在App里使用WKWebView
来加载网页时,我们希望实现一个在页面顶部的进度条,来表示网页加载的进度。而恰好WKWebView
的实例有一个estimatedProgress
属性,我们可以在此基础上使用KVO来实现。
添加观察者
第一步,是要正确地声明变量。因为KVO是在Objective-C中提供的,要在Swift中使用,被观察的属性必须添加@objc
和dynamic
关键词,来确保可以正确地被观察到。
// 声明属性,我们的网页视图
@objc var webview: MKWebView!
// 创建私有变量,用于添加观察者时创建context参数
private var progressContext = 0
第二步,就是在适合的位置添加观察者。一般来说在viewDidLoad
中添加就可以。
// 订阅观察webView.estimatedProgress属性
webView.addObserver(self, forKeyPath: #keyPath(estimatedProgress), options: [.new, .old], context: &progressContext)
对于方法func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?)
简单介绍下其参数:
-
方法消息接受者,就是被监听的对象。不过就上面的代码而言,
webView
同样也是self
当前controller的属性,所以这条消息也可以发送给self
,但是keyPath
就要相应地修改为keyPath(webView.estimatedProgress)
。 -
观察者,即订阅观察的对象,在被观察者数值变化时收到通知。一般来说,就是当前的controller。
-
keyPath,即相对于接受者对象,需要观察的属性。可以直接用明确的字符串
"estimatedProgress"
来替代#keyPath(estimatedProgress)
,但那样直接操作字符串出现打错,还是用#keyPath构造比较简单。 -
options,这里是接收对象时,选择接收的累类型。总共有4种,需要接受就添加其enum值进入数组参数传入:
-
.new
,接收到变化后的新数值。 -
.old
,接收到变化前的老数值。 -
initial
,即要求立刻返回通知给观察者,在注册观察者方法返回之前。 -
.prior
,即是否需要在数值变化前和变化后各发送一条通知,而不是默认的只在变化后发送通知。
-
-
context,这里的环境变量,一般用于在不同的观察者在观察相同的
keyPath
时用于区分。上面的添加观察者代码中,我其实没必要传入context
,只是为了演示如何创建与传入context
。// 首先是声明私有变量 private var myContext = 0 // 然后直接使用`&myContext`作为`context`参数传入。
接收被观察者通知并响应处理
我们的目的是实现进度条,因此需要先添加一条进度条。
func setUpWebView() {
webView.frame = view.bounds
webView.navigationDelegate = self
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
guard let url = URL(string: urlString!) else {
print("url is nil")
return
}
webView.load(URLRequest(url: url))
// 创建名为progress的进度条
let progress = UIView(frame: CGRect(x: 0, y: 0, width: webView.frame.width, height: 3))
webView.addSubview(progress)
// 之前已经提前声明了progressLayer作为实例变量,方便作为进度条修改
progressLayer = CALayer()
progressLayer.backgroundColor = APPColor.orange.cgColor
progress.layer.addSublayer(progressLayer!)
view.addSubview(webView)
// 设置进度条进度的方法,这里直接在打开网页时,设置10%的加载进度,让页面加载看起来更快
progressLayer!.frame = CGRect(x: 0, y: 0, width: webView.frame.width * 0.1, height: 3)
}
进度条配置好了,下面就可以设置监听方法,来处理进度条了。
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(webView.estimatedProgress) && context == &progressContext {
guard let changes = change else { return }
// 请注意这里读取options中数值的方法
let newValue = changes[NSKeyValueChangeKey.newKey] as? Double ?? 0
let oldValue = changes[NSKeyValueChangeKey.oldKey] as? Double ?? 0
// 因为我们已经设置了进度条为0.1,所以只有在进度大于0.1后再进行变化
if newValue > oldValue && newValue > 0.1 {
progressLayer.frame = CGRect(x: 0, y: 0, width: webView.frame.width * CGFloat(newValue), height: 3)
}
// 当进度为100%时,隐藏progressLayer并将其初始值改为0
if newValue == 1.0 {
let time1 = DispatchTime.now() + 0.4
let time2 = time1 + 0.1
DispatchQueue.main.asyncAfter(deadline: time1) {
weak var weakself = self
weakself?.progressLayer.opacity = 0
}
DispatchQueue.main.asyncAfter(deadline: time2) {
weak var weakself = self
weakself?.progressLayer.frame = CGRect(x: 0, y: 0, width: 0, height: 3)
}
}
}
移除观察者
在不需要监听时,或者至少在观察者
要被释放之前,需要移除观察者身份。
在viewDidDisappear
或者其他适当的位置,调用:
removeObserver(self, forKeyPath: #keyPath(webView.estimatedProgress))
这样利用KVO实现加载进度条的目的已经达成了。
更Swifty的实现方式:Block-based KVO
在Swift4里,官方推荐了另外Key-value Oberservation的实现方式。简单来说,就是创建一个变量observation、给obervation赋值。赋值实现了既添加观察者又实现响应通知的功能。最后在不需要观察时,直接把observation设置为nil
即可。
针对上面的进度加载条,实现代码如下:
// 声明变量,被观察的属性依然还需要添加@objc和dynamic
@objc var webView = WKWebView()
var progressLayer: CALayer!
var progressObervation: NSKeyValueObservation?
// 设置观察
func setupObserver() {
// 请务必注意方法的写法
progressObservation = observe(\.webView.estimatedProgress, options: [.old, .new], changeHandler: { (self, change) in
let newValue = change.newValue ?? 0
let oldValue = change.oldValue ?? 0
print("new value is \(newValue)")
print("new value is \(oldValue)")
if newValue > oldValue && newValue > 0.1 {
print("time to reset new value")
weak var weakself = self
weakself?.progressLayer.frame = CGRect(x: 0, y: 0, width: (weakself?.webView.frame.width)! * CGFloat(newValue), height: 3)
}
if newValue == 1.0 {
let time1 = DispatchTime.now() + 0.4
let time2 = time1 + 0.1
DispatchQueue.main.asyncAfter(deadline: time1) {
weak var weakself = self
weakself?.progressLayer.opacity = 0
}
DispatchQueue.main.asyncAfter(deadline: time2) {
weak var weakself = self
weakself?.progressLayer.frame = CGRect(x: 0, y: 0, width: 0, height: 3)
}
}
})
}
func destroyObserver() {
progressObservation = nil
progressObserver?.invalidate()
}
这样看来是不是很简单?而且一个NSKeyValueObservation对象只负责观察一个keyPath
,非常清晰。同时只用一行代码和闭包,更简洁。
这里介绍下给observation赋值的方法参数。
-
receiver,即方法的接受者。上面的方法可以改成:
progressObserver = webView.observe(\.estimatedProgress, options: [.old, .new], changeHandler: { (webView, change) { // code }
-
keyPath,这里的
keyPath
与上文中的keyPath
接收的参数类型不同。这里是KeyPath
类型,而上面addObserver
方法中的keyPath
是字符串。写法是\.property
,这里的property
是相对于receiver
的,所以当receiver
是controller时,keyPath
就是\.webView.estimatedProgress
;而当receiver
是webView
时,keyPath则是\.estimatedProgress
。 -
options,与上文一样,传入可选的
.new, .old, .initial, .prior
。可不传入options,这样的话,不能从闭包中接收到的change
里的newValue
和oldValue
都是0。 -
closure,闭包接收2个参数,即
receiver
和作为NSKeyValueObservedChange
类型的change
。从change可以读取其newValue
和oldValue
。
最后关于停止监听,有两个办法可选:
// 销毁
progressObserver = nil
// 不销毁,仅仅停止监听
progressObserver?.invalidate()
如果不需要停止,可以不用处理,也不用刻意去移除监听,controller
作为observation
的owner
会自动处理。
本人初学,有错误或疏漏之处,欢迎斧正!
参考文档:
网友评论