KVO,即Key-value observation,是蘋果提供的一種機(jī)制,它可以使監(jiān)聽(tīng)對(duì)象在被監(jiān)聽(tīng)對(duì)象的數(shù)值發(fā)生改變時(shí)收到通知,進(jìn)而去進(jìn)行響應(yīng)的處理。
KVO實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,主要的流程只有3個(gè):
添加觀察者
在監(jiān)聽(tīng)方法中處理監(jiān)聽(tīng)結(jié)果
監(jiān)聽(tīng)結(jié)束后移除觀察者
下面我們用一個(gè)實(shí)際的例子來(lái)說(shuō)明一下這3個(gè)步驟。在App里使用WKWebView來(lái)加載網(wǎng)頁(yè)時(shí),我們希望實(shí)現(xiàn)一個(gè)在頁(yè)面頂部的進(jìn)度條,來(lái)表示網(wǎng)頁(yè)加載的進(jìn)度。而恰好WKWebView的實(shí)例有一個(gè)estimatedProgress屬性,我們可以在此基礎(chǔ)上使用KVO來(lái)實(shí)現(xiàn)。
添加觀察者
第一步,是要正確地聲明變量。因?yàn)镵VO是在Objective-C中提供的,要在Swift中使用,被觀察的屬性必須添加@objc和dynamic關(guān)鍵詞,來(lái)確??梢哉_地被觀察到。
// 聲明屬性,我們的網(wǎng)頁(yè)視圖@objcvarwebview:MKWebView!// 創(chuàng)建私有變量,用于添加觀察者時(shí)創(chuàng)建context參數(shù)privatevarprogressContext=0
第二步,就是在適合的位置添加觀察者。一般來(lái)說(shuō)在viewDidLoad中添加就可以。
// 訂閱觀察webView.estimatedProgress屬性webView.addObserver(self,forKeyPath:#keyPath(estimatedProgress),options:[.new,.old],context:&progressContext)
對(duì)于方法func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?)簡(jiǎn)單介紹下其參數(shù):
方法消息接受者,就是被監(jiān)聽(tīng)的對(duì)象。不過(guò)就上面的代碼而言,webView同樣也是self當(dāng)前controller的屬性,所以這條消息也可以發(fā)送給self,但是keyPath就要相應(yīng)地修改為keyPath(webView.estimatedProgress)。
觀察者,即訂閱觀察的對(duì)象,在被觀察者數(shù)值變化時(shí)收到通知。一般來(lái)說(shuō),就是當(dāng)前的controller。
keyPath,即相對(duì)于接受者對(duì)象,需要觀察的屬性??梢灾苯佑妹鞔_的字符串"estimatedProgress"來(lái)替代#keyPath(estimatedProgress),但那樣直接操作字符串出現(xiàn)打錯(cuò),還是用#keyPath構(gòu)造比較簡(jiǎn)單。
options,這里是接收對(duì)象時(shí),選擇接收的累類型??偣灿?種,需要接受就添加其enum值進(jìn)入數(shù)組參數(shù)傳入:
.new,接收到變化后的新數(shù)值。
.old,接收到變化前的老數(shù)值。
initial,即要求立刻返回通知給觀察者,在注冊(cè)觀察者方法返回之前。
.prior,即是否需要在數(shù)值變化前和變化后各發(fā)送一條通知,而不是默認(rèn)的只在變化后發(fā)送通知。
context,這里的環(huán)境變量,一般用于在不同的觀察者在觀察相同的keyPath時(shí)用于區(qū)分。上面的添加觀察者代碼中,我其實(shí)沒(méi)必要傳入context,只是為了演示如何創(chuàng)建與傳入context。
// 首先是聲明私有變量privatevarmyContext=0// 然后直接使用`&myContext`作為`context`參數(shù)傳入。
接收被觀察者通知并響應(yīng)處理
我們的目的是實(shí)現(xiàn)進(jìn)度條,因此需要先添加一條進(jìn)度條。
funcsetUpWebView(){webView.frame=view.bounds? ? webView.navigationDelegate=selfwebView.autoresizingMask=[.flexibleWidth,.flexibleHeight]guardleturl=URL(string:urlString!)else{print("url is nil")return}webView.load(URLRequest(url:url))// 創(chuàng)建名為progress的進(jìn)度條letprogress=UIView(frame:CGRect(x:0,y:0,width:webView.frame.width,height:3))webView.addSubview(progress)// 之前已經(jīng)提前聲明了progressLayer作為實(shí)例變量,方便作為進(jìn)度條修改progressLayer=CALayer()progressLayer.backgroundColor=APPColor.orange.cgColor? ? progress.layer.addSublayer(progressLayer!)view.addSubview(webView)// 設(shè)置進(jìn)度條進(jìn)度的方法,這里直接在打開(kāi)網(wǎng)頁(yè)時(shí),設(shè)置10%的加載進(jìn)度,讓頁(yè)面加載看起來(lái)更快progressLayer!.frame=CGRect(x:0,y:0,width:webView.frame.width*0.1,height:3)}
進(jìn)度條配置好了,下面就可以設(shè)置監(jiān)聽(tīng)方法,來(lái)處理進(jìn)度條了。
overridefuncobserveValue(forKeyPath keyPath:String?,of object:Any?,change:[NSKeyValueChangeKey:Any]?,context:UnsafeMutableRawPointer?){ifkeyPath==#keyPath(webView.estimatedProgress)&&context==&progressContext{guardletchanges=changeelse{return}//? 請(qǐng)注意這里讀取options中數(shù)值的方法letnewValue=changes[NSKeyValueChangeKey.newKey]as?Double??0letoldValue=changes[NSKeyValueChangeKey.oldKey]as?Double??0// 因?yàn)槲覀円呀?jīng)設(shè)置了進(jìn)度條為0.1,所以只有在進(jìn)度大于0.1后再進(jìn)行變化ifnewValue>oldValue&&newValue>0.1{progressLayer.frame=CGRect(x:0,y:0,width:webView.frame.width*CGFloat(newValue),height:3)}// 當(dāng)進(jìn)度為100%時(shí),隱藏progressLayer并將其初始值改為0ifnewValue==1.0{lettime1=DispatchTime.now()+0.4lettime2=time1+0.1DispatchQueue.main.asyncAfter(deadline:time1){weakvarweakself=selfweakself?.progressLayer.opacity=0}DispatchQueue.main.asyncAfter(deadline:time2){weakvarweakself=selfweakself?.progressLayer.frame=CGRect(x:0,y:0,width:0,height:3)}}}
移除觀察者
在不需要監(jiān)聽(tīng)時(shí),或者至少在觀察者要被釋放之前,需要移除觀察者身份。
在viewDidDisappear或者其他適當(dāng)?shù)奈恢?,調(diào)用:
removeObserver(self,forKeyPath:#keyPath(webView.estimatedProgress))
這樣利用KVO實(shí)現(xiàn)加載進(jìn)度條的目的已經(jīng)達(dá)成了。
更Swifty的實(shí)現(xiàn)方式:Block-based KVO
在Swift4里,官方推薦了另外Key-value Oberservation的實(shí)現(xiàn)方式。簡(jiǎn)單來(lái)說(shuō),就是創(chuàng)建一個(gè)變量observation、給obervation賦值。賦值實(shí)現(xiàn)了既添加觀察者又實(shí)現(xiàn)響應(yīng)通知的功能。最后在不需要觀察時(shí),直接把observation設(shè)置為nil即可。
針對(duì)上面的進(jìn)度加載條,實(shí)現(xiàn)代碼如下:
// 聲明變量,被觀察的屬性依然還需要添加@objc和dynamic@objcvarwebView=WKWebView()varprogressLayer:CALayer!varprogressObervation:NSKeyValueObservation?// 設(shè)置觀察funcsetupObserver(){// 請(qǐng)務(wù)必注意方法的寫法progressObservation=observe(\.webView.estimatedProgress,options:[.old,.new],changeHandler:{(self,change)inletnewValue=change.newValue??0letoldValue=change.oldValue??0print("new value is\(newValue)")print("new value is\(oldValue)")ifnewValue>oldValue&&newValue>0.1{print("time to reset new value")weakvarweakself=selfweakself?.progressLayer.frame=CGRect(x:0,y:0,width:(weakself?.webView.frame.width)!*CGFloat(newValue),height:3)}ifnewValue==1.0{lettime1=DispatchTime.now()+0.4lettime2=time1+0.1DispatchQueue.main.asyncAfter(deadline:time1){weakvarweakself=selfweakself?.progressLayer.opacity=0}DispatchQueue.main.asyncAfter(deadline:time2){weakvarweakself=selfweakself?.progressLayer.frame=CGRect(x:0,y:0,width:0,height:3)}}})}funcdestroyObserver(){progressObservation=nilprogressObserver?.invalidate()}
這樣看來(lái)是不是很簡(jiǎn)單?而且一個(gè)NSKeyValueObservation對(duì)象只負(fù)責(zé)觀察一個(gè)keyPath,非常清晰。同時(shí)只用一行代碼和閉包,更簡(jiǎn)潔。
這里介紹下給observation賦值的方法參數(shù)。
receiver,即方法的接受者。上面的方法可以改成:
progressObserver=webView.observe(\.estimatedProgress,options:[.old,.new],changeHandler:{(webView,change){// code}
keyPath,這里的keyPath與上文中的keyPath接收的參數(shù)類型不同。這里是KeyPath類型,而上面addObserver方法中的keyPath是字符串。寫法是\.property,這里的property是相對(duì)于receiver的,所以當(dāng)receiver是controller時(shí),keyPath就是\.webView.estimatedProgress;而當(dāng)receiver是webView時(shí),keyPath則是\.estimatedProgress。
options,與上文一樣,傳入可選的.new, .old, .initial, .prior??刹粋魅雘ptions,這樣的話,不能從閉包中接收到的change里的newValue和oldValue都是0。
closure,閉包接收2個(gè)參數(shù),即receiver和作為NSKeyValueObservedChange類型的change。從change可以讀取其newValue和oldValue。
最后關(guān)于停止監(jiān)聽(tīng),有兩個(gè)辦法可選:
// 銷毀progressObserver=nil// 不銷毀,僅僅停止監(jiān)聽(tīng)progressObserver?.invalidate()
如果不需要停止,可以不用處理,也不用刻意去移除監(jiān)聽(tīng),controller作為observation的owner會(huì)自動(dòng)處理。
作者:火石君
鏈接:http://www.itdecent.cn/p/919fefa588c2
來(lái)源:簡(jiǎn)書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。