- 此文读者:iOS 开发者
- 此文深度:粗浅
- 此文目的:解决 TableView 嵌套 WKWebView 高度问题
- 环境配置:Xcode10.0 & macOS10.13.6 & 弱网环境
前言
项目做多了,难免会有些需要和富文本打交道地方。展示一个富文本可以使用多种技术方案,不过多个方案之间各有自己的特性,这就需要开发人员进行技术的筛选。因本司编辑员常用网页样式,故而一些长篇的图文当中就需要 WebView 作为容器进行展示了。
如果展示是单纯的H5介绍页,使用一个纯 WebView 进行展示是非常合适的。不过当展示界面混合了 Native 控件,事情就变得不太容易起来。通常这样的页面同新闻的详情页一样,一个 TableView 中嵌套了包含 WebView 的 Cell 或 TableHeaderView,且要求整个 TableView 滑动起来自然 & 连贯。
- 连贯,即要求 TableView 整体内容不能有截断,所以 TableView 中的各个部分要求自适应其高度;
- 自然,即要求 TableView 在滑动的时候内容随着操作手势明确地上下滚动 不能出现掉帧、卡顿现象,所以 耗时操作 & 响应事件 就需要格外关注;
- 轻巧,即要求 实现方式较为简便,便于其他开发者的维护;
准备
我们需要创建一个类似新闻详情页的Demo:
- 主页头部展示 HTML ,宽度与屏幕宽度一致,高度随内容需要完全展开;
- 尾部是推荐文章的单元,点击该单元可以切换头部的内容,就像下图一样。
其详情页的层级如下:(已上传到GitHub,为了便于方案演变,本人随开发做了 commit 记录,读者可根据个人需求在各个 commit 版本中切换)
- TableView
- WebCell:用于 HTML 展示;
- SectionCell:用于 模块标题 展示;
- ArticleCell:用于 推荐单元 展示;
- ToolView
- 手势点击事件
基础版:
类型:纯文本
参考:《人间失格》单元
详情页展示的是纯文本,我们仅需在 WebViw 代理webView(_, didFinish)
方法通过执行document.body.scrollHeight
JS代码注入获取文档高度,再将高度反馈给 TableView 进行刷新即可,该高度即为文档渲染的正确高度。
1 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { |
请参考Demo中「基础版」commit 节点。
观察者模式:
类型:少量图文混排
参考:《迟暮》单元
当详情页展示的是少量的图文混排后,因为图片的加载是一件耗时操作的事情,我们通常将其设定为懒加载模式,当页面内的资源加载完毕后我们再获取其高度,才为正确的文档高度。所以在webView(_, didFinish)
里获取其高度也不是准确的,该代理方法是 WebView 载入 HTML 文档完成后的回调,并不等于该 HTML 完全渲染完后的回调。
根据以上判断,我们需要捕捉 HTML 渲染变化的信号。而 HTML 渲染动作直接影响到的是 WebView scrollView.contentSize
属性,每当该值发生变化代表的是当前 HTML文档 已渲染到的位置。
我们可以使用观察者模式来监听这个属性的变化,当该属性发生变化时我们需要及时调整容器的高度并将其反映给 TableView 进行刷新。
1 | fileprivate lazy var webView: WKWebView = { |
同 contentSize
一样,我们也可以使用更高效的 loading
来进行监控,该属性用于表达 WebView 的加载状态, Apple 文档中对 isLoading
属性的描述如下:
A Boolean value indicating whether the view is currently loading content.
@discussion @link WKWebView @/link is key-value observing (KVO) compliant for this property.
修改后的代码,如下:
1 | fileprivate lazy var webView: WKWebView = { |
通过打印这两个观察者属性的调用动作,可以发现 contentSize 比 loading 调用的次数多,且在滚动 TableView 的时候 contentSize 是随即更新无论是否真有变化,而 loading 自加载完成后不再调用,两者计算的高度值一样。
如果使用观察者模式的话,建议选用 loading
模式:
其一 监听加载的动作更符合渲染状态发生这一事实;
其二 如果被认定为已加载完成 contentSize 就不会再变,无需持续观察。
虽然网络稍好的时候用以上方法均可以实现获取文档的正确高度,但一旦网络极差的情况下上面的方式都失效了。
请参考Demo中「观察者模式」commit 节点。
* JS 监听:
记得上周五下班回家在地铁上开开心心等待第二天的 ChinaJoy,拿起小手机看看资讯消息,然后一条老板的微信消息@me,老板给我截了图~某详情页展示不全。虽然对这个问题熟悉得不能再熟悉也清楚要做什么,但还是为之一怔,因为和老板交流的不多(有时候他看着我,但目光已经透过了我到达后面的同事),为了不打破被老板忽略的角落形象,决定好好想想策略。
类型:大量图文混排
参考:《它们一边鄙视,一边用自己的方式照顾我们》单元
经过了之前的技术探索,觉得从 WebView 中找到监察 HTML 页面渲染完成的状态并不可靠,如果 HTML能够主动发送消息给我就好了。
按照这种想法,搜索了 「HTML 加载完成事件、HTML 图片懒加载完成事件……」,最终查到,在 HTML DOM 中 Event 有个函数 onload 是用于一张页面或一幅图像完成加载时所执行的,我们需要监听所有的 img
标签 或 body
标签,然后在这个方法里发个消息给 WebKit 然后进行拦截即可。
接下来我们要做两件事:
- 在 WebView 里注册一个方法,用以接收 HTML DOM 的事件;
- 在 HTML 里补充 JS 脚本,用以发送消息给 WebView ;
1. 在 WebView 里注册一个方法
- 配置 WKWebViewConfiguration ,为其注册一个 ScriptMessageHandler ;
- 实现 WKScriptMessageHandler 代理方法,拦截 你所注册的 ScriptMessageHandler;
1 | fileprivate lazy var webView: WKWebView = { |
2. 添加以下 JS 代码在 \</ body> 标签之后
- 使用
document.getElementsByTagName('img')
会获取 DOM 目录下所有的 img 标签;- 使用
window.webkit.messageHandlers.<你所注册的方法名>.postMessage('数据消息')
给 webkit 发送消息;
1 | <script type="text/javascript"> |
本文仅作为实际工程的应用,不涉及任何知识体系,想了解更多,请查看 WebKit 的开源代码 ╮(╯_╰)╭。