WKWebView高度计算

avatar
iOS 开发工程师 @抖音视界有限公司

近期有一个需求需要在View的某一个区域插入一段H5的内容,这段内容是服务端下发的,相信很多同僚都做过类似的需求,现在就把本人做这个需求的经历记录下来,希望对大家有所帮助。

这个需求的难点是下发的H5内容高度是变化的,根据服务端下发内容的不同,WebView会有不同的高度。所以关键点也是在拿到服务端内容进行WebView加载完成之后,计算出WebView的高度,并且设置为对应的值即可。

只包含文本的H5

在网上找了很多资料,大多数的都是如下的方式,在 func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) 代理方法中添加如下代码

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    webView.evaluateJavaScript("document.body.offsetHeight") { result, _ in
        if let height = result as? CGFloat {
            var frame = webView.frame
            frame.size.height = height
            webView.frame =  frame
        }
    }
}

上面的方式在大多数的时候是有效正确的,但我遇到了一些情况还是计算的有问题。经过不断的查阅资料,还是找到了一个更准确的计算方式,方式基本和上面是一样的,调用的JS不一样,使用 document.documentElement.offsetHeight,代码如下

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    webView.evaluateJavaScript("document.documentElement.offsetHeight") { result, _ in
        if let height = result as? CGFloat {
            var frame = webView.frame
            frame.size.height = height
            webView.frame =  frame
        }
    }
}

至于原因,大致可以如下理解:document.documentElement.offsetHeight >= document.body.offsetHeight,具体详情可以参考 这里,这里描述了 offsetHeight 在各个节点及各个浏览器的一些差异。

还有一点要注意:初始化webView的时候,高度要设置为0,宽度需要设置成需要的宽度

包含文本和图片的H5

正解

上面的方法对于没有图片的H5是完全够用的,但是如果H5中有图片之后,上面的方案就会有一些缺陷,如果图片加载过慢的话,会导致获取的高度偏小,因为有些H5的<img>标签是没有预先设置size的,而且这种情况特别多。我们上面的代码获取高度是在整个DOM加载完后获取高度,此时就有很有可能<img>中的图片没有加载完,此时获取的高度是没有包括<img>中图片的高度的,故而不准确。

此时我们可以在H5中注入一段代码,如下:

<script>
    function imagesLoad() { 
        let images = document.getElementsByTagName('img'); 
        for (var i = 0; i <= images.length - 1; i++) { 
            (images[i]).onload = function() { 
                let height = document.documentElement.scrollHeight;
                // 给 wkwebkit 一个通知 imageLoaded,并把高度传过去
                window.webkit.messageHandlers.imageLoaded.postMessage(height); 
            } 
        }
    } 
</script>

这段代码是用于WKWebView调用的,添加了一个函数imagesLoad(),它给所有的<img>标签添加一个onload函数,此函数是在<img>获取到图片之后调用,这样在所有的图片加载完成后都能获取到最新的高度,然后给 wkwebkit 一个通知 imageLoaded,并把高度传给iOS端。

接下来看一下iOS端如何处理,在 func webViewDidFinishLoad(_ webView: BDWebView) 中添加如下代码

func webViewDidFinishLoad(_ webView: BDWebView) {
    // 其他业务逻辑...
    webView.evaluateJavaScript("imagesLoad();")
}

imageLoaded是需要如下处理的

// 要先初始化 configuration 和 userContentController,此处略
webView.configuration.userContentController.add(self, name: "imageLoaded")

还要实现 WKScriptMessageHandler 协议

extension BDWebViewManager: WKScriptMessageHandler {
	func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
	    if message.name == "imageLoaded" {
	        if let height = (message.body as? CGFloat) {
	            // 此处设置webview的高度
	        }
	    }
	}
}

至此即可获取WKWebView的准确高度。

KVO误解

查资料时有使用KVO的方式获取WebView高度,此方法经本人测试是有问题的,如果webview每次加载的内容都比上一次更多,webview的高度更高的话是可以的,或者加载一次性的H5内容也是可以的,但是如果使用同一个WebView多次加载不同的内容,就会出现一些问题,比如第一次加载H5的内容获取的高度是1000,而第二次加载的内容高度是800,此时下方或多出约200的空白区域,所以如果使用同一个WebView多次加载不同的内容,请慎用此方法。