UIWebView获取实际内容高度

前言

获取 UIWebView 实际内容高度,是个大坑。网上的方法五花八门,也有通病,跟随我一步步踩坑。获取 UIWebView 的实际内容高度,看我这篇文章就够了。

背景

为什么要获取 UIWebView 的实际内容高度,相信很多人是为了做混合开发。Native + HTML5,将 UIWebView 嵌套在 UIScrollView 里,由 UIScrollView 控制滚动。这样就需要 Webview 高度自适应内容,也就是让 UIWebView 的控件高度跟它的内容高度一致,才可以显示完整的页面。

踩坑

在网上查了很久,看到最多的是这个方法:

在代理方法 - (void)webViewDidFinishLoad:(UIWebView *)webView 中,获取高度:

1
CGFloat height = [[webView stringByEvaluatingJavaScriptFromString:@"document.body.offsetHeight"] floatValue];

这段代码是不正确的,body 获取到的 offsetHeight 为显示区域的高度,需要改为 scrollHeight

于是,这个方法就改成:

1
CGFloat height = [[webView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight"] floatValue];

还有另外一个办法,是获取 contentSize 的高度:

1
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.clientHeightdocument.body.scrollHeight。这种获取方式很容易因为 H5 内容的不同,样式的不同而不能准确地拿到高度,包括利用 NJKWebViewProgress

  • 通过给 H5 的内容 content 最外层包一层标签加载,这时候会有两个问题:

    • 很难保证 H5 的所有样式都能对应到 content 中。
    • 加过标签后的 H5 内容会被默认再包一层 document 标签,还有就是加最外层的标签不能保证对原始标签显示没有影响,这样就很难保证大部分 H5 展示没问题。

后来经过多次测试,发现了比较优雅的解决方案:动态获取 UIWebView 高度。

优雅地解决

如何能在 webViewDidFinishLoad 之后获取到网页内容高度的变化?

答案:KVO

给 webView 的 scrollView 的 contentSize 属性添加监听,每当内容发生变化,contentSize 一定会跟着变,捕获这个变动,在监听方法中实现原本写在 webViewDidFinishLoad 中的代码,也就是获取最新的内容高度,并赋值给 webView 的高度,或者赋值给 webView 的高度约束的 constant 的代码。

KVO 注册

1
[self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];

在回调方法里做更新UI操作

1
2
3
4
5
6
7
8
- (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
}
}

移除监听对象

在页面消失时记得 remove 监听对象。在viewWillDisappear还是dealloc方法移除要根据情况而定。

1
[webView.scrollView removeObserver:self forKeyPath:@"contentSize" context:nil];

更优雅地解决

每次 KVO 监听都要写 observeValueForKeyPath:ofObject:change:context: 这个方法,也要写移除监听对象的代码,好像也不是很优雅,又想了一下,RAC

使用 RAC 来实现 KVO,就只需要一段如此简单的代码:

1
2
3
4
[RACObserve(self.webView.scrollView, contentSize) subscribeNext:^(id x) {
// self.webView.height = contentHeight;
self.webViewHeightContraint.constant = contentHeight; // autoLayout
}];

混合开发注意点和参考代码

在 cell 中使用 webView 获取高度不准确的解决办法跟上面一样,只不过需要注意 cell 中使用 webView 涉及到 cell 重用,会导致滑动列表时 webView 多次加载,影响性能,建议缓存高度,至于具体怎么优化就仁者见仁智者见智吧。

以上方案会频繁更改 webView 高度,当 H5 内容非常多时,有几率会闪退,因为内存溢出。此时建议将 UIWebView 改为 WKWebView,性能大幅度提升,也不会有内存问题。

在这种混合开发的需求下,总结一下可能会用到的相关代码。

设置 webview 不可滚动

1
((UIScrollView *)[self.webView.subviews objectAtIndex:0]).scrollEnabled = NO;

自适应富文本内容

1
#define DKWebContent(Content) [NSString stringWithFormat:@"%@%@",DKWebContentPrefix,Content]

常量

1
2
3
4
5
6
// 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>";

使用

1
[self.webView loadHTMLString:DKWebContent(content) baseURL:nil];

后话

至此,UIWebView 高度自适应内容以及持续自适应就完美实现了。讲了这么多其实核心就是:监听webView.scrollView.contentSize的变化然后调整 webView 的高度。最后,还是建议使用 WKWebView,即使它也有坑,但会比 UIWebView 好太多。

  • 本文作者: Bingo
  • 本文链接: https://blog.bingo.ren/41.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!