UIWebView获取实际内容高度
# 前言
获取 UIWebView 实际内容高度,是个大坑。网上的方法五花八门,也有通病,跟随我一步步踩坑。获取 UIWebView 的实际内容高度,看我这篇文章就够了。
# 背景
为什么要获取 UIWebView 的实际内容高度,相信很多人是为了做混合开发。Native + HTML5,将 UIWebView 嵌套在 UIScrollView 里,由 UIScrollView 控制滚动。这样就需要 Webview 高度自适应内容,也就是让 UIWebView 的控件高度跟它的内容高度一致,才可以显示完整的页面。
# 踩坑
在网上查了很久,看到最多的是这个方法:
在代理方法 - (void)webViewDidFinishLoad:(UIWebView *)webView
中,获取高度:
CGFloat height = [[webView stringByEvaluatingJavaScriptFromString:@"document.body.offsetHeight"] floatValue];
这段代码是不正确的,body 获取到的 offsetHeight
为显示区域的高度,需要改为 scrollHeight
。
于是,这个方法就改成:
CGFloat height = [[webView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight"] floatValue];
还有另外一个办法,是获取 contentSize 的高度:
CGFloat height = webView.scrollView.contentSize.height;
当然还有其他办法,比如包一层 div 标签,用来获取这个 div 的高度等,这其中的坑就更多了,包括内容实际高度还与像素和点的比有关系。
类似的方法有很多,具体可以参考两个链接:
《完美方案——iOS的WebView自适应内容高度》 《iOS计算UIWebView的高度和iOS8之后的WKWebView的高度问题》
以上这几种方案或多或少都能解决一定场景下的高度计算,但是都会有些问题。无论是 JavaScript 获取,还是 contentSize 获取,最后结果都难以获取到准确高度。
有时获得的高度不够,有时候又会多出来一点。我经常遇到的问题就是获取到的高度在 iPhone 6s plus 上刚好显示完整,而到了 iPhone 6s 上就会多出一段空白。而有时候却是不够显示,但是下拉刷新一下,就又正常了。
# 解决
我把代码反复看了几遍,最终定位到 webViewDidFinishLoad:
这个方法上。于是上网查询,终于知道了原因:我想当然的认为 webViewDidFinishLoad:
回调之时就是 webview 加载完成。实际上并不是,页面并不一定完全展现完成,可能有图片还未加载出来,导致此时获取的高度并不是最终高度,会小于真实高度。过会儿图片加载出来后,浏览器会根据 CSS 重新排版,而我们在这之前给它 set 了一个错误高度,自然就会导致显示不完整。
记住:
代理方法 webViewDidFinishLoad:
回调的时候并不能说明 webview 加载完成。
关于获取到内容高度有偏差的情况,简书上的有一条解决办法的评论是这么说的:
原因是代理执行的时候,内容没有真正的加载完,就会导致获取的高度是错的,我用了NJKWebviewProgress来检测内容加载进度,在加载完之后获取高度。
NJKWebviewProgress,看了 README 后觉得应该可行。但也挺麻烦的,专门为了解决一个问题而引入了一个第三方框架,一点都不优雅。事实证明我的这种思想是对的,就算引入了也不一定能解决这个问题。
# 坑中坑
如果是按照上面的解决方法把坑给填了,千万不要高兴。因为这只是解决了坑里面的一个坑,你依然还在坑里面。主要有下面两个问题:
通过调用 JS 方法,获取高度,例如:
document.body.clientHeight
,document.body.scrollHeight
。这种获取方式很容易因为 H5 内容的不同,样式的不同而不能准确地拿到高度,包括利用NJKWebViewProgress
。通过给 H5 的内容 content 最外层包一层标签加载,这时候会有两个问题:
- 很难保证 H5 的所有样式都能对应到 content 中。
- 加过标签后的 H5 内容会被默认再包一层 document 标签,还有就是加最外层的标签不能保证对原始标签显示没有影响,这样就很难保证大部分 H5 展示没问题。
后来经过多次测试,发现了比较优雅的解决方案:动态获取 UIWebView 高度。
# 优雅地解决
如何能在 webViewDidFinishLoad 之后获取到网页内容高度的变化?
答案:KVO
给 webView 的 scrollView 的 contentSize 属性添加监听,每当内容发生变化,contentSize 一定会跟着变,捕获这个变动,在监听方法中实现原本写在 webViewDidFinishLoad 中的代码,也就是获取最新的内容高度,并赋值给 webView 的高度,或者赋值给 webView 的高度约束的 constant 的代码。
# KVO 注册
[self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
# 在回调方法里做更新UI操作
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"contentSize"]) {
CGFloat contentHeight = self.webView.scrollView.contentSize.height;
// self.webView.height = contentHeight;
self.webViewHeightContraint.constant = contentHeight; // autoLayout
}
}
2
3
4
5
6
7
8
# 移除监听对象
在页面消失时记得 remove 监听对象。在viewWillDisappear
还是dealloc
方法移除要根据情况而定。
[webView.scrollView removeObserver:self forKeyPath:@"contentSize" context:nil];
# 更优雅地解决
每次 KVO 监听都要写 observeValueForKeyPath:ofObject:change:context:
这个方法,也要写移除监听对象的代码,好像也不是很优雅,又想了一下,RAC!
使用 RAC 来实现 KVO,就只需要一段如此简单的代码:
[RACObserve(self.webView.scrollView, contentSize) subscribeNext:^(id x) {
// self.webView.height = contentHeight;
self.webViewHeightContraint.constant = contentHeight; // autoLayout
}];
2
3
4
# 混合开发注意点和参考代码
在 cell 中使用 webView 获取高度不准确的解决办法跟上面一样,只不过需要注意 cell 中使用 webView 涉及到 cell 重用,会导致滑动列表时 webView 多次加载,影响性能,建议缓存高度,至于具体怎么优化就仁者见仁智者见智吧。
以上方案会频繁更改 webView 高度,当 H5 内容非常多时,有几率会闪退,因为内存溢出。此时建议将 UIWebView 改为 WKWebView,性能大幅度提升,也不会有内存问题。
在这种混合开发的需求下,总结一下可能会用到的相关代码。
# 设置 webview 不可滚动
((UIScrollView *)[self.webView.subviews objectAtIndex:0]).scrollEnabled = NO;
# 自适应富文本内容
# 宏
#define DKWebContent(Content) [NSString stringWithFormat:@"%@%@",DKWebContentPrefix,Content]
# 常量
// DKConst.h
/** H5内容前缀 */
FOUNDATION_EXPORT NSString * const DKWebContentPrefix;
// DKConst.m
NSString * const DKWebContentPrefix = @"<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><style>img {max-width:100%;height:auto;}</style></head>";
2
3
4
5
6
# 使用
[self.webView loadHTMLString:DKWebContent(content) baseURL:nil];
# 后话
至此,UIWebView 高度自适应内容以及持续自适应就完美实现了。讲了这么多其实核心就是:监听webView.scrollView.contentSize
的变化然后调整 webView 的高度。最后,还是建议使用 WKWebView,即使它也有坑,但会比 UIWebView 好太多。