在客户端中如果需要实现曝光打点的需求,经常会遇到各种各样的问题,例如:该在什么时机去打点;复用的 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 的复用