iOS 客戶端檢查曝光組件 (Intersection Observer for iOS)

在客戶端中如果需要實(shí)現(xiàn)曝光打點(diǎn)的需求,經(jīng)常會(huì)遇到各種各樣的問(wèn)題,例如:該在什么時(shí)機(jī)去打點(diǎn);復(fù)用的 view 打點(diǎn)混亂;切換界面或者切換 APP 前后臺(tái)需不需要打點(diǎn);等等。

ZHIntersectionObserver 就是為了解決這個(gè)問(wèn)題而誕生的。

具體 Demo 演示效果可以先看進(jìn)倉(cāng)庫(kù)查看:

Github地址:?https://github.com/zhoon/ZHIntersectionObserver

熱身:Intersection Observer API

說(shuō)起?Intersection Observer,應(yīng)該有些同學(xué)已經(jīng)聽說(shuō)過(guò)了,在 Web 上已經(jīng)有這樣的一套 Web API 如下:https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

Intersection Observer 的作用顧名思義就是監(jiān)聽 UI 元素是否跟某個(gè)父元素相交,常用于曝光監(jiān)控。Web API 的?Intersection Observer 使用方式如下:

初始化一個(gè) IntersectionObserver 并且傳入一個(gè)回調(diào) callback,和配置項(xiàng) options(下文會(huì)做具體介紹):

設(shè)置需要監(jiān)聽的元素,然后開始監(jiān)聽:


當(dāng)被監(jiān)聽元素跟 root 的相交(intersection)情況發(fā)生變化的時(shí)候,callback 就會(huì)被調(diào)用, 我們可以在 callback 里面處理一些業(yè)務(wù),例如曝光打點(diǎn)等等,callback 會(huì)傳回一個(gè)參數(shù) entries 給業(yè)務(wù)判斷當(dāng)前的 intersection 情況:


延伸:Intersection Observer for iOS

作為 iOS 開發(fā),雖然我們平時(shí)大部分業(yè)務(wù)都不需要曝光打點(diǎn),但是當(dāng)遇到一些這樣的需求的時(shí)候,這個(gè)問(wèn)題就變的比較棘手(例如一些推薦需求,推薦內(nèi)容需要依賴客戶端的曝光或者點(diǎn)擊來(lái)實(shí)現(xiàn)動(dòng)態(tài)推薦)。

我們平時(shí)在打曝光的時(shí)候,傳統(tǒng)的做法一般是初始化某個(gè) view 的時(shí)候去打個(gè)點(diǎn),或者在 UITableView willDisplay 等 delegate 的時(shí)候去曝光,但是這些方法都有明顯的缺點(diǎn),例如:

沒有比較精準(zhǔn)的計(jì)算當(dāng)前 view 是否真的在界面內(nèi)導(dǎo)致曝光不準(zhǔn)確

重復(fù)曝光,例如 UITableView 被多次 reload;

對(duì)復(fù)用的 View 不友好,例如 UITableViewCell;

曝光代碼夾雜在各種業(yè)務(wù)代碼中難維護(hù);

無(wú)法控制曝光時(shí)間,快速滾動(dòng)也會(huì)被當(dāng)作曝光

除了以上一些比較明顯的問(wèn)題,可能還會(huì)有其他大大小小坑等著我們。

針對(duì)以上這些問(wèn)題,Intersection Observer for iOS 誕生了:https://github.com/zhoon/ZHIntersectionObserver

參考?Intersection Observer 的 API,ZHIntersectionObserver 實(shí)現(xiàn)了在 iOS 平臺(tái)的 Intersection Observer 功能,通過(guò)這些功能,我們可以方便的處理例如曝光打點(diǎn)的問(wèn)題,ZHIntersectionObserver 的特點(diǎn)包括:

支持設(shè)置多個(gè)臨界點(diǎn)(thresholds)

支持控制列表滾動(dòng)檢查曝光的頻率(throttle)

View 被移除或者 hidden 或者 alpha 變化支持自動(dòng)檢查曝光

App 切換前臺(tái)或者后臺(tái)支持自動(dòng)檢查曝光

支持設(shè)置曝光時(shí)長(zhǎng)(intersectionDuration)

支持?jǐn)?shù)據(jù)變化自動(dòng)檢查曝光

兼容 UITableViewCell 等的復(fù)用控件

使用起來(lái)也非常簡(jiǎn)單,只需要初始化一個(gè) IntersectionObserverContainerOptions 和 一個(gè) IntersectionObserverTargetOptions 并且賦值給對(duì)應(yīng)的 containerView 和 targetView 即可:

UIView*targetView=[[UIView alloc]init];

UIView*containerView=[[UIView alloc]init];

UIEdgeInsets rootMargin=UIEdgeInsetsMake(CGRectGetMaxY(self.navigationController.navigationBar.frame),0,0,0);

__weak__typeof(self)weakSelf=self;

IntersectionObserverContainerOptions*containerOptions=[IntersectionObserverContainerOptions initOptionsWithScope:@"Example1"rootMargin:rootMargin thresholds:@[@1]containerView:containerView intersectionDuration:300callback:^(NSString*_Nonnull scope,NSArray<IntersectionObserverEntry*>*_Nonnull entries){

__strong__typeof(weakSelf)strongSelf=weakSelf;

for(NSInteger i=0;i<entries.count;i++) {

????IntersectionObserverEntry*entry=entries[i];

????if (entry.isInsecting) {// 進(jìn)入可是區(qū)域} else { // 移出可視區(qū)域}

}

}];

containerView.intersectionObserverContainerOptions=containerOptions;

IntersectionObserverTargetOptions*targetOptions=[IntersectionObserverTargetOptions initOptionsWithScope:@"Example1"targetView:targetView];

targetView.intersectionObserverTargetOptions=targetOptions;

參數(shù)詳解(IntersectionObserverContainerOptions)

scope

作用域,一般一個(gè) observer 對(duì)應(yīng)一個(gè)作用域,某個(gè)作用域的 container 觸發(fā)檢查,只有對(duì)應(yīng)作用域的 target 才會(huì)被通知。例如某個(gè) UITableView 和它的 UITableViewCell 是同一個(gè) scope,當(dāng)列表滾動(dòng)的時(shí)候,會(huì)自動(dòng)發(fā)送檢查事件,那么只有這個(gè)列表里面的 cell 才會(huì)做曝光檢查。

rootMargin

控制父容器的邊距,例如當(dāng)我們把整個(gè)界面作為容器的時(shí)候,頂部可能有 navBar 或者底部有 tabBar,想要讓 targetView 在可視區(qū)域才算曝光(也就是 navBar 之下和 tabBar 之上),那么就可以把 rootMargin 的 top 和 bottom 分別設(shè)置為 navBar 和 tabBar 的高度。

thresholds

設(shè)置臨界點(diǎn),有時(shí)有不想要整個(gè) targetView 出現(xiàn)在可視區(qū)域才算曝光,而是有一像素出現(xiàn)或者某個(gè)面積的某個(gè)百分比,則可以設(shè)置 thresholds來(lái)實(shí)現(xiàn),這個(gè)參數(shù)是一個(gè)數(shù)組,也就是某個(gè)設(shè)置的每個(gè) threshold 零界點(diǎn)觸發(fā)都會(huì)有 callback,默認(rèn)值是 @[@1],也就是整個(gè) targetView 進(jìn)入可是區(qū)域才算曝光。

throttle

節(jié)流參數(shù),對(duì)于 containerView 是 scrollView,ZHIntersectionObserver 會(huì)在滾動(dòng)的時(shí)候自動(dòng)去檢查,節(jié)流參數(shù)可以控制檢查的頻率,避免頻率太高影響性能。

intersectionDuration

曝光時(shí)間,這個(gè)是 web api 所沒有支持的,即要求 targetView 需要曝光多長(zhǎng)時(shí)間才會(huì)觸發(fā) callback。曝光問(wèn)題里面有個(gè)比較難處理的就是要不要認(rèn)為 targetView 需要在可視區(qū)域曝光一定的時(shí)長(zhǎng)才算曝光,例如快速滾動(dòng)的列表或者一閃而過(guò)的網(wǎng)絡(luò)數(shù)據(jù)覆蓋本地?cái)?shù)據(jù),這些能不能算曝光呢?intersectionDuration 可以控制 targetView 需要在可視區(qū)域停留一定時(shí)長(zhǎng)才算曝光,默認(rèn) 600 ms。

參數(shù)詳解(IntersectionObserverTargetOptions)

scope

同 IntersectionObserverContainerOptions

dataKey

非常重要的一個(gè)參數(shù),特別是對(duì)于復(fù)用的 view,例如 UITableViewCell。如果 view 復(fù)用,那么只能通過(guò) dataKey 來(lái)區(qū)分當(dāng)前是不同的數(shù)據(jù),當(dāng)某個(gè) view 被復(fù)用了,需要 update 一下當(dāng)前 view 所對(duì)應(yīng)的 IntersectionObserverTargetOptions 對(duì)應(yīng)的 dataKey,值一般是當(dāng)前數(shù)據(jù)的 id 或者組合字符串,標(biāo)記當(dāng)前的數(shù)據(jù)獨(dú)一無(wú)二。

data

當(dāng)前 dataKey 對(duì)應(yīng)的 data,ZHIntersectionObserver 不會(huì)使用,只會(huì)在 IntersectionObserverEntry 里面透?jìng)鹘o callback,方便業(yè)務(wù)使用。

原理及思考

1、所有的曝光都是基于 containerView 和 targetView 的,也就是說(shuō)我們檢測(cè)的是?targetView 在?containerView 容器中的曝光情況,而不是相對(duì)于當(dāng)前應(yīng)用界面的 window。如果是你的?containerView 也發(fā)生位置或者大小的變化,那應(yīng)該通過(guò)同坐 roomMargin 來(lái)響應(yīng)這種變化。

2、什么時(shí)機(jī)觸發(fā)曝光檢查?當(dāng) containerView 和 targetView 被指定一個(gè) options 的時(shí)候,會(huì)自動(dòng)觸發(fā)一次曝光檢查,然后會(huì)自動(dòng)給 view 綁定一個(gè) KVO,監(jiān)聽 view 的 alpha、hidden、bounds、position 的變化,只要這些屬性變化,都會(huì)重新檢查曝光。另外,一般我們一個(gè) containerView 就對(duì)應(yīng)一個(gè) observer,所以當(dāng)屬性變化的時(shí)候,只會(huì)觸發(fā) containerView 或者?targetView 所屬的 observer 進(jìn)行檢查,其他不相關(guān)的不會(huì)檢查,從而避免不必要的消耗。

3、對(duì)于 ScrollView,除了上述的觸發(fā)時(shí)機(jī),還需要在滾動(dòng)的時(shí)候進(jìn)行檢查。當(dāng)給 containerView 指定 options 時(shí),ZHIntersectionObserver 判斷如果當(dāng)前是 UIScrollView,則會(huì)自動(dòng)給 ScrollView 綁定一個(gè)監(jiān)聽 contentOffset 的 KVO,當(dāng)列表滾動(dòng)的時(shí)候就會(huì)觸發(fā)曝光檢查,為了避免頻繁觸發(fā)檢查,給 KVO 加了截流函數(shù)限制,提高性能。

4、除了上面的兩種時(shí)機(jī),還有兩種:一種是切換界面(例如 push pop present dismiss 或者切 tab 等等,這種不會(huì)出發(fā)上面屬性 KVO 的變化,但是 view.window 會(huì)變化,所以通過(guò) hook didMoveToWindow 方法來(lái)實(shí)現(xiàn)觸發(fā)時(shí)機(jī));另外一種是 APP 切換前臺(tái)后臺(tái)(ZHIntersectionObserver 內(nèi)部通過(guò)監(jiān)聽 APP 生命周期的 notification 實(shí)現(xiàn));

5、如何實(shí)現(xiàn)限制曝光時(shí)長(zhǎng)?如果要說(shuō)一個(gè) view 怎樣才算曝光,那么是要求這個(gè) view 需要在屏幕內(nèi)停留一定的時(shí)間,而不是快速的滾過(guò)屏幕也算,這種屬于無(wú)效數(shù)據(jù),但是這種在平時(shí)的業(yè)務(wù)中實(shí)現(xiàn)起來(lái)比較繁瑣。ZHIntersectionObserver 提供一個(gè) intersectionDuration 參數(shù)控制 view 需要在屏幕內(nèi)曝光多長(zhǎng)時(shí)間才算曝光。實(shí)現(xiàn)的原理是,當(dāng) view 第一次曝光的時(shí)候,不會(huì)馬上調(diào)用 callback,而是 delay 一個(gè)時(shí)長(zhǎng)(業(yè)務(wù)設(shè)置),經(jīng)過(guò)這個(gè) delay 之后重新把之前需要調(diào)用 callback 的 entries 拿出來(lái),重新檢查當(dāng)前 entry 是否還是維持跟 delay 前一樣的狀態(tài),如果是則調(diào)用 callback,否則廢棄 entry。

6、如何解決 view 復(fù)用的問(wèn)題?所謂復(fù)用問(wèn)題就是 view 沒變,但是 view 承載的數(shù)據(jù)變了,那我們需要曝光的是數(shù)據(jù)而不是這個(gè) view,這個(gè) view 只是給我們判斷當(dāng)前是否 visible 而已。解決這個(gè)問(wèn)題的關(guān)鍵是給 targetView 的 options 加一個(gè) dataKey,dataKey 對(duì)于每一份不同的數(shù)據(jù)都是唯一值的,當(dāng)更新了 view 的數(shù)據(jù),需要 update 一下 options 的?dataKey,ZHIntersectionObserver 會(huì)自動(dòng)觸發(fā)曝光檢查,檢查會(huì)判斷是否?dataKey 變化了來(lái)決定需不需要重新檢查曝光和調(diào)用 callback。

其他使用場(chǎng)景

Intersection Observer 除了用在曝光打點(diǎn),還可以用在其他場(chǎng)景例如:

圖片懶加載

無(wú)限滾動(dòng),甚至實(shí)現(xiàn) view 的復(fù)用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容