理解KVO - 用Swift在WKWebView中添加进度条

KVO,即Key-value observation,是苹果提供的一种机制,它可以使监听对象在被监听对象的数值发生改变时收到通知,进而去进行响应的处理。

KVO实现起来比较简单,主要的流程只有3个:

  1. 添加观察者
  2. 在监听方法中处理监听结果
  3. 监听结束后移除观察者

下面我们用一个实际的例子来说明一下这3个步骤。在App里使用WKWebView来加载网页时,我们希望实现一个在页面顶部的进度条,来表示网页加载的进度。而恰好WKWebView的实例有一个estimatedProgress属性,我们可以在此基础上使用KVO来实现。

添加观察者

第一步,是要正确地声明变量。因为KVO是在Objective-C中提供的,要在Swift中使用,被观察的属性必须添加@objcdynamic关键词,来确保可以正确地被观察到。

// 声明属性,我们的网页视图
@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;而当receiverwebView时,keyPath则是\.estimatedProgress

  • options,与上文一样,传入可选的.new, .old, .initial, .prior。可不传入options,这样的话,不能从闭包中接收到的change里的newValueoldValue都是0。

  • closure,闭包接收2个参数,即receiver和作为NSKeyValueObservedChange类型的change。从change可以读取其newValueoldValue

最后关于停止监听,有两个办法可选:

// 销毁
progressObserver = nil

// 不销毁,仅仅停止监听
progressObserver?.invalidate()

如果不需要停止,可以不用处理,也不用刻意去移除监听,controller作为observationowner会自动处理。

本人初学,有错误或疏漏之处,欢迎斧正!

参考文档:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容