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

在客戶端中如果需要實現曝光打點的需求,經常會遇到各種各樣的問題,例如:該在什么時機去打點;復用的 view 打點混亂;切換界面或者切換 APP 前后臺需不需要打點;等等。

ZHIntersectionObserver 就是為了解決這個問題而誕生的。

具體 Demo 演示效果可以先看進倉庫查看:

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

熱身:Intersection Observer API

說起?Intersection Observer,應該有些同學已經聽說過了,在 Web 上已經有這樣的一套 Web API 如下:https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

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

初始化一個 IntersectionObserver 并且傳入一個回調 callback,和配置項 options(下文會做具體介紹):

設置需要監聽的元素,然后開始監聽:


當被監聽元素跟 root 的相交(intersection)情況發生變化的時候,callback 就會被調用, 我們可以在 callback 里面處理一些業務,例如曝光打點等等,callback 會傳回一個參數 entries 給業務判斷當前的 intersection 情況:


延伸:Intersection Observer for iOS

作為 iOS 開發,雖然我們平時大部分業務都不需要曝光打點,但是當遇到一些這樣的需求的時候,這個問題就變的比較棘手(例如一些推薦需求,推薦內容需要依賴客戶端的曝光或者點擊來實現動態推薦)。

我們平時在打曝光的時候,傳統的做法一般是初始化某個 view 的時候去打個點,或者在 UITableView willDisplay 等 delegate 的時候去曝光,但是這些方法都有明顯的缺點,例如:

沒有比較精準的計算當前 view 是否真的在界面內導致曝光不準確

重復曝光,例如 UITableView 被多次 reload;

對復用的 View 不友好,例如 UITableViewCell;

曝光代碼夾雜在各種業務代碼中難維護;

無法控制曝光時間,快速滾動也會被當作曝光

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

針對以上這些問題,Intersection Observer for iOS 誕生了:https://github.com/zhoon/ZHIntersectionObserver

參考?Intersection Observer 的 API,ZHIntersectionObserver 實現了在 iOS 平臺的 Intersection Observer 功能,通過這些功能,我們可以方便的處理例如曝光打點的問題,ZHIntersectionObserver 的特點包括:

支持設置多個臨界點(thresholds)

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

View 被移除或者 hidden 或者 alpha 變化支持自動檢查曝光

App 切換前臺或者后臺支持自動檢查曝光

支持設置曝光時長(intersectionDuration)

支持數據變化自動檢查曝光

兼容 UITableViewCell 等的復用控件

使用起來也非常簡單,只需要初始化一個 IntersectionObserverContainerOptions 和 一個 IntersectionObserverTargetOptions 并且賦值給對應的 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) {// 進入可是區域} else { // 移出可視區域}

}

}];

containerView.intersectionObserverContainerOptions=containerOptions;

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

targetView.intersectionObserverTargetOptions=targetOptions;

參數詳解(IntersectionObserverContainerOptions)

scope

作用域,一般一個 observer 對應一個作用域,某個作用域的 container 觸發檢查,只有對應作用域的 target 才會被通知。例如某個 UITableView 和它的 UITableViewCell 是同一個 scope,當列表滾動的時候,會自動發送檢查事件,那么只有這個列表里面的 cell 才會做曝光檢查。

rootMargin

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

thresholds

設置臨界點,有時有不想要整個 targetView 出現在可視區域才算曝光,而是有一像素出現或者某個面積的某個百分比,則可以設置 thresholds來實現,這個參數是一個數組,也就是某個設置的每個 threshold 零界點觸發都會有 callback,默認值是 @[@1],也就是整個 targetView 進入可是區域才算曝光。

throttle

節流參數,對于 containerView 是 scrollView,ZHIntersectionObserver 會在滾動的時候自動去檢查,節流參數可以控制檢查的頻率,避免頻率太高影響性能。

intersectionDuration

曝光時間,這個是 web api 所沒有支持的,即要求 targetView 需要曝光多長時間才會觸發 callback。曝光問題里面有個比較難處理的就是要不要認為 targetView 需要在可視區域曝光一定的時長才算曝光,例如快速滾動的列表或者一閃而過的網絡數據覆蓋本地數據,這些能不能算曝光呢?intersectionDuration 可以控制 targetView 需要在可視區域停留一定時長才算曝光,默認 600 ms。

參數詳解(IntersectionObserverTargetOptions)

scope

同 IntersectionObserverContainerOptions

dataKey

非常重要的一個參數,特別是對于復用的 view,例如 UITableViewCell。如果 view 復用,那么只能通過 dataKey 來區分當前是不同的數據,當某個 view 被復用了,需要 update 一下當前 view 所對應的 IntersectionObserverTargetOptions 對應的 dataKey,值一般是當前數據的 id 或者組合字符串,標記當前的數據獨一無二。

data

當前 dataKey 對應的 data,ZHIntersectionObserver 不會使用,只會在 IntersectionObserverEntry 里面透傳給 callback,方便業務使用。

原理及思考

1、所有的曝光都是基于 containerView 和 targetView 的,也就是說我們檢測的是?targetView 在?containerView 容器中的曝光情況,而不是相對于當前應用界面的 window。如果是你的?containerView 也發生位置或者大小的變化,那應該通過同坐 roomMargin 來響應這種變化。

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

3、對于 ScrollView,除了上述的觸發時機,還需要在滾動的時候進行檢查。當給 containerView 指定 options 時,ZHIntersectionObserver 判斷如果當前是 UIScrollView,則會自動給 ScrollView 綁定一個監聽 contentOffset 的 KVO,當列表滾動的時候就會觸發曝光檢查,為了避免頻繁觸發檢查,給 KVO 加了截流函數限制,提高性能。

4、除了上面的兩種時機,還有兩種:一種是切換界面(例如 push pop present dismiss 或者切 tab 等等,這種不會出發上面屬性 KVO 的變化,但是 view.window 會變化,所以通過 hook didMoveToWindow 方法來實現觸發時機);另外一種是 APP 切換前臺后臺(ZHIntersectionObserver 內部通過監聽 APP 生命周期的 notification 實現);

5、如何實現限制曝光時長?如果要說一個 view 怎樣才算曝光,那么是要求這個 view 需要在屏幕內停留一定的時間,而不是快速的滾過屏幕也算,這種屬于無效數據,但是這種在平時的業務中實現起來比較繁瑣。ZHIntersectionObserver 提供一個 intersectionDuration 參數控制 view 需要在屏幕內曝光多長時間才算曝光。實現的原理是,當 view 第一次曝光的時候,不會馬上調用 callback,而是 delay 一個時長(業務設置),經過這個 delay 之后重新把之前需要調用 callback 的 entries 拿出來,重新檢查當前 entry 是否還是維持跟 delay 前一樣的狀態,如果是則調用 callback,否則廢棄 entry。

6、如何解決 view 復用的問題?所謂復用問題就是 view 沒變,但是 view 承載的數據變了,那我們需要曝光的是數據而不是這個 view,這個 view 只是給我們判斷當前是否 visible 而已。解決這個問題的關鍵是給 targetView 的 options 加一個 dataKey,dataKey 對于每一份不同的數據都是唯一值的,當更新了 view 的數據,需要 update 一下 options 的?dataKey,ZHIntersectionObserver 會自動觸發曝光檢查,檢查會判斷是否?dataKey 變化了來決定需不需要重新檢查曝光和調用 callback。

其他使用場景

Intersection Observer 除了用在曝光打點,還可以用在其他場景例如:

圖片懶加載

無限滾動,甚至實現 view 的復用

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容