如何确定WKWebView的内容大小?

87

我正在尝试在运行iOS 8及更高版本时,用WKWebView实例替换动态分配的UIWebView实例,并且我找不到确定WKWebView内容大小的方法。

我的网页视图嵌入在较大的UIScrollView容器中,因此我需要确定网页视图的理想大小。这将使我能够修改它的框架以显示所有HTML内容,而无需在网页视图内滚动,并且我将能够为滚动视图容器设置正确的高度(通过设置scrollview.contentSize)。

我已经尝试了sizeToFit和sizeThatFits,但都没有成功。以下是创建WKWebView实例并将其添加到容器滚动视图的代码:

// self.view is a UIScrollView sized to something like 320.0 x 400.0.
CGRect wvFrame = CGRectMake(0, 0, self.view.frame.size.width, 100.0);
self.mWebView = [[[WKWebView alloc] initWithFrame:wvFrame] autorelease];
self.mWebView.navigationDelegate = self;
self.mWebView.scrollView.bounces = NO;
self.mWebView.scrollView.scrollEnabled = NO;

NSString *s = ... // Load s from a Core Data field.
[self.mWebView loadHTMLString:s baseURL:nil];

[self.view addSubview:self.mWebView];

这里是一个实验性的didFinishNavigation方法:

- (void)webView:(WKWebView *)aWebView
                             didFinishNavigation:(WKNavigation *)aNavigation
{
    CGRect wvFrame = aWebView.frame;
    NSLog(@"original wvFrame: %@\n", NSStringFromCGRect(wvFrame));
    [aWebView sizeToFit];
    NSLog(@"wvFrame after sizeToFit: %@\n", NSStringFromCGRect(wvFrame));
    wvFrame.size.height = 1.0;
    aWebView.frame = wvFrame;
    CGSize sz = [aWebView sizeThatFits:CGSizeZero];
    NSLog(@"sizeThatFits A: %@\n", NSStringFromCGSize(sz));
    sz = CGSizeMake(wvFrame.size.width, 0.0);
    sz = [aWebView sizeThatFits:sz];
    NSLog(@"sizeThatFits B: %@\n", NSStringFromCGSize(sz));
}

这里是生成的输出:

2014-12-16 17:29:38.055 App[...] original wvFrame: {{0, 0}, {320, 100}}
2014-12-16 17:29:38.055 App[...] wvFrame after sizeToFit: {{0, 0}, {320, 100}}
2014-12-16 17:29:38.056 App[...] wvFrame after sizeThatFits A: {320, 1}
2014-12-16 17:29:38.056 App[...] wvFrame after sizeThatFits B: {320, 1}

sizeToFit调用没有效果,而sizeThatFits始终返回高度为1。


更新:我仍在寻找解决方案。如果我通过[self.mWebView loadRequest:req]加载远程内容,则在didFinishNavigation中可以通过self.mWebView.scrollView.contentSize获得大小。但是,如果我通过[self.mWebView loadHTMLString:s]加载我的内容,则直到稍后才能获得大小。使用带有dataURL的loadRequest无法解决问题。而且我不知道“稍后”是什么时候。 - Mark Smith
1
我想知道这个问题是否有一个有意义的答案?考虑到任何时候网页都可能根据窗口大小调整其内容大小,那么我们如何才能将窗口大小更改为其内容大小呢?我认为 WKWebView 没有提供 intrinsicContentSize 的原因是没有单一、有意义的“内在”大小。 - wardw
2021年:添加 addUserScript 然后使用 ResizeObserver。 - dklt
20个回答

148

我认为我阅读了这个主题的每一个答案,但我只得到了解决方案的一部分。大部分时间我都在尝试实现@davew描述的KVO方法,它有时有效,但大多数时候会在WKWebView容器的内容下留下空白。我还实施了@David Beck的建议,并使容器高度为0,从而避免了如果容器高度大于内容高度可能出现问题的可能性。尽管如此,我还是偶尔遇到了空白的空间。

对我来说,“contentSize”观察器存在很多缺陷。我对Web技术没有太多经验,因此我无法回答这种解决方案的问题,但我发现如果我只在控制台中打印高度而不做任何操作(例如重新调整约束),它会跳转到某个数字(例如5000),然后跳回到比最高的那个数字更低的数字(例如2500-这就是正确的数字)。如果我将高度约束设置为从“contentSize”获取的高度,它将自动设置为它获得的最高数字,并且永远不会被调整为正确的数字,这也是@David Beck评论中提到的问题。

经过许多实验,我终于找到了适合我的解决方案:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    self.webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
        if complete != nil {
            self.webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
                self.containerHeight.constant = height as! CGFloat
            })
        }

        })
}

当然,正确设置限制条件非常重要,这样scrollView就可以根据containerHeight约束进行调整大小。

事实证明,当我想要的时候didFinish navigation方法从来没有被调用过,但是设置了document.readyState步骤之后,下一个步骤(document.body.offsetHeight)在正确的时刻被调用,为我返回了正确的高度值。


6
根据这个gist答案,您可能希望使用document.body.scrollHeight,而不是之前提到的方法。请注意,这是建议并应谨慎使用。 - Zev Eisenberg
7
在我的情况下并不起作用,无论是document.body.offsetHeight 还是 document.body.scrollHeight 都提供了错误的高度。 - Ratul Sharker
46
我已经用上述代码使它正常工作,但是只有在给我的HTML字符串添加一些元数据后才成功:<meta name="viewport" content="width=device-width, initial-scale=1">。 - andrei
1
对我来说,WKWebViewdocument.body.offsetHeight 返回错误的高度,有什么想法吗?https://stackoverflow.com/questions/54187194/wkwebview-document-body-offsetheight-returns-wrong-height-why - János
24
在 iOS 13 上,document.body.scrollHeight 不起作用,因此我使用了 document.documentElement.scrollHeight,这对我有用。 - Chirag Kothiya
显示剩余10条评论

29

你可以使用键值观察(KVO)...

在你的 ViewController 中:

- (void)viewDidLoad {
    ...
    [self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
}


- (void)dealloc
{
    [self.webView.scrollView removeObserver:self forKeyPath:@"contentSize" context:nil];
}


- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (object == self.webView.scrollView && [keyPath isEqual:@"contentSize"]) {
        // we are here because the contentSize of the WebView's scrollview changed.

        UIScrollView *scrollView = self.webView.scrollView;
        NSLog(@"New contentSize: %f x %f", scrollView.contentSize.width, scrollView.contentSize.height);
    }
}

这样可以节省使用JavaScript的步骤,并让您了解所有更改的情况。


6
但这也存在一些缺陷:如果内容变得比网页视图的高度小,内容大小将与网页视图框架相同。 - David Beck
12
自iOS 10.3以来,使用这种类型的KVO会导致页面末尾处出现长空白页。有没有人有解决这种情况的方法? - Raditya Kurnianto

22

最近我自己也不得不处理这个问题。最终,我使用了 Chris McClenaghan 提出的解决方案的修改版

实际上,他的原始解决方案非常好且适用于大多数简单情况。然而,它仅在带有文本的页面上对我有效。它可能也适用于具有静态高度的图片页面。但是,当您拥有大小由 max-heightmax-width 属性定义的图像时,它绝对行不通。

这是因为那些元素可以在页面加载后被调整大小之后。因此,实际上,在 onLoad 中返回的高度总是正确的。但它只对特定的实例正确。解决方法是监视 body 高度的变化并对其做出响应。

监视 document.body 的大小调整

var shouldListenToResizeNotification = false
lazy var webView:WKWebView = {
    //Javascript string
    let source = "window.onload=function () {window.webkit.messageHandlers.sizeNotification.postMessage({justLoaded:true,height: document.body.scrollHeight});};"
    let source2 = "document.body.addEventListener( 'resize', incrementCounter); function incrementCounter() {window.webkit.messageHandlers.sizeNotification.postMessage({height: document.body.scrollHeight});};"
    
    //UserScript object
    let script = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
    
    let script2 = WKUserScript(source: source2, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
    
    //Content Controller object
    let controller = WKUserContentController()
    
    //Add script to controller
    controller.addUserScript(script)
    controller.addUserScript(script2)
    
    //Add message handler reference
    controller.add(self, name: "sizeNotification")
    
    //Create configuration
    let configuration = WKWebViewConfiguration()
    configuration.userContentController = controller
    
    return WKWebView(frame: CGRect.zero, configuration: configuration)
}()

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    guard let responseDict = message.body as? [String:Any],
    let height = responseDict["height"] as? Float else {return}
    if self.webViewHeightConstraint.constant != CGFloat(height) {
        if let _ = responseDict["justLoaded"] {
            print("just loaded")
            shouldListenToResizeNotification = true
            self.webViewHeightConstraint.constant = CGFloat(height)
        }
        else if shouldListenToResizeNotification {
            print("height is \(height)")
            self.webViewHeightConstraint.constant = CGFloat(height)
        }
        
    }
}

这个解决方案是我能想到的最优雅的。但是,你需要注意两件事。

首先,在加载URL之前,你应该将shouldListenToResizeNotification设置为false。在加载的URL可以快速更改的情况下,需要额外的逻辑。当发生这种情况时,旧内容的通知出现重叠的情况,以某种原因的形式通知新内容。为了防止这种行为,我创建了这个变量。它确保一旦我们开始加载新内容,我们就不再处理旧内容的通知,并且只在新内容加载后恢复处理调整大小通知。

但最重要的是,你需要注意这点:

如果你采用这个解决方案,你需要考虑到,如果你改变WKWebView的大小,除了通知报告的大小之外的任何大小 - 通知将再次被触发。

要小心这一点,因为很容易陷入无限循环。例如,如果你决定通过使高度等于报告高度+一些额外的填充来处理通知:

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard let responseDict = message.body as? [String:Float],
        let height = responseDict["height"] else {return}
        self.webViewHeightConstraint.constant = CGFloat(height+8)
    }

正如您所见,因为我将8添加到报告的高度中,所以在此操作完成后,我的body大小将发生变化,并且通知将再次发布。

请警惕这种情况,否则你应该没问题。

如果您发现此解决方案存在任何问题,请告诉我 - 我自己也依赖它,所以最好知道我没有注意到的故障!


Andriy,我无法感谢你更多。这将为我节省几个小时的谷歌搜索和尝试。只是一点额外的信息:当meta viewport标签设置为width=device-width, initial-scale=1.0, shrink-to-fit=no时,这对我有用,并且我删除了在shouldListenToResizeNotification = true下设置的约束条件。 - Tamás Sengel
我也能够使用这种方法使其工作。谢谢!只是好奇,是否有人尝试过在此处记录的webview.scrollView.addObserver方法?https://dev59.com/b1wX5IYBdhLWcg3w4S3R#33289730 - Blue Waters
2
这个可以工作。记得通过webView.configuration.userContentController.removeScriptMessageHandler干净地删除它,否则Web视图会保留您的self引用,导致内存泄漏。 - Kunal
3
Source2事件监听器从未被调用,我嵌入了Instagram帖子,每次获取到的source1高度都不正确。 - Chlebta
我看到了和Chlebta一样的问题 - 这对于因社交嵌入而调整大小的页面无效。 - Zack

9

对我来说没问题

extension TransactionDetailViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.webviewHeightConstraint.constant = webView.scrollView.contentSize.height
        }
    }
}

5
这个解决方案不能保证在调用'didFinish'后的0.1秒内加载完成。 - Trzy Gracje
1
做得好。最佳解决方案。谢谢伙计。 - Mani murugan
如果我把我的iPhone从竖屏旋转到横屏,则高度变得太高。 - user5306470
1
.async更改为asyncAfter(deadline: .now() + 0.1)对我有用。谢谢! - oğuz

8

你也可以使用evaluateJavaScript来获取WKWebView的内容高度。

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [webView evaluateJavaScript:@"Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight)"
              completionHandler:^(id _Nullable result, NSError * _Nullable error) {
                  if (!error) {
                      CGFloat height = [result floatValue];
                      // do with the height

                  }
              }];
}

7
请尝试以下操作。在您实例化WKWebView实例的任何地方,添加类似以下内容的东西:
    //Javascript string
    NSString * source = @"window.webkit.messageHandlers.sizeNotification.postMessage({width: document.width, height: document.height});";

    //UserScript object
    WKUserScript * script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];

    //Content Controller object
    WKUserContentController * controller = [[WKUserContentController alloc] init];

    //Add script to controller
    [controller addUserScript:script];

    //Add message handler reference
    [controller addScriptMessageHandler:self name:@"sizeNotification"];

    //Create configuration
    WKWebViewConfiguration * configuration = [[WKWebViewConfiguration alloc] init];

    //Add controller to configuration
    configuration.userContentController = controller;

    //Use whatever you require for WKWebView frame
    CGRect frame = CGRectMake(...?);

    //Create your WKWebView instance with the configuration
    WKWebView * webView = [[WKWebView alloc] initWithFrame:frame configuration:configuration];

    //Assign delegate if necessary
    webView.navigationDelegate = self;

    //Load html
    [webView loadHTMLString:@"some html ..." baseURL:[[NSBundle mainBundle] bundleURL]];

然后在遵循WKScriptMessageHandler协议的任何类中添加一个类似以下方法的方法来处理消息:

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    CGRect frame = message.webView.frame;
    frame.size.height = [[message.body valueForKey:@"height"] floatValue];
    message.webView.frame = frame;}

这对我有用。

如果您的文档中不仅有文本,还可能需要像这样包装javascript以确保加载了所有内容:

@"window.onload=function () { window.webkit.messageHandlers.sizeNotification.postMessage({width: document.width, height: document.height});};"

注意:此解决方案不涉及文档的持续更新。


userContentController:didReceiveScriptMessage: 方法中,我收到了一个空字典。 :( - Timur Bernikovich
只有将 document.widthdocument.height 更改为 window.innerWidthwindow.innerHeight 才能使其正常工作,这就是为什么你会得到一个空字典,@TimurBernikowich。 - paulvs
1
我对JavaScript不是很了解,但是document.body.scrollHeight可以准确获取高度。document.heightwindow.innerHeight都是0。同时需要使用window.onload=function () {...} - Ryan Poolos

7

大多数答案使用 "document.body.offsetHeight"。

这会隐藏 body 的最后一个对象。

我通过使用监听 WKWebview "contentSize" 变化的 KVO 观察者来克服了这个问题,然后运行以下代码:

self.webView.evaluateJavaScript(
    "(function() {var i = 1, result = 0; while(true){result = 
    document.body.children[document.body.children.length - i].offsetTop + 
    document.body.children[document.body.children.length - i].offsetHeight;
    if (result > 0) return result; i++}})()",
    completionHandler: { (height, error) in
        let height = height as! CGFloat
        self.webViewHeightConstraint.constant = height
    }
)

这段代码可能不是最美观的,但它对我很有效。


1
+1 JS!它处理了一个非常重要的边缘情况!对于之前比其内容短(例如有垂直滚动条)但现在变得更高的Webview,它可以正确地测量Webview的高度。这种情况发生在将设备从竖屏旋转到横屏时,您的Webview突然变宽。在这种情况下,即使内容现在较短(由于增加的宽度),document.body.scrollOffset仍会返回先前(较高)的值,并留下很多空白处于底部。虽然我不喜欢KVO方法,但didFinish代理方法更加清晰。 - m_katsifarakis

7
我发现在这里hlung的回答,通过扩展WKWebView如下所示是我最简单、最有效的解决方案: https://gist.github.com/pkuecuekyan/f70096218a6b969e0249427a7d324f91 他的评论如下:
"不错!对我来说,不是设置webView.frame,而是设置自动布局intrinsicContentSize。"
他的代码如下:
import UIKit
import WebKit

class ArticleWebView: WKWebView {

  init(frame: CGRect) {
    let configuration = WKWebViewConfiguration()
    super.init(frame: frame, configuration: configuration)
    self.navigationDelegate = self
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override var intrinsicContentSize: CGSize {
    return self.scrollView.contentSize
  }

}

extension ArticleWebView: WKNavigationDelegate {

  func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    webView.evaluateJavaScript("document.readyState", completionHandler: { (_, _) in
      webView.invalidateIntrinsicContentSize()
    })
  }

}

你在这里如何创建初始框架? - Kyle Browning

6
你需要等待webview加载完成。这里有一个我用过的可行示例。 WKWebView内容加载函数从未被调用 然后在webview加载完成后,你可以通过以下方式确定所需的高度。
func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) {

   println(webView.scrollView.contentSize.height)

}

3
谢谢!但在我的情况下,didFinishNavigation太早了(我得到的大小是0, 0)。我没有看到一个简单的方法来在WKWebView实例完成加载其内容后调用,因此我将尝试使用JS-> Native消息来解决这个问题。看起来我需要使用WKUserContentController并实现WKScriptMessageHandler协议。 - Mark Smith
3
这个方法可以正常运行,但需要一个小技巧。你需要再等待十分之一秒才能获得实际的内容大小。 - Borut Tomazin
1
那个解决方案不正确。我有0.1秒的延迟,但在某些情况下这是不够的。如果你有更多的内容,你必须一次又一次地增加延迟。 - Makalele

3

这是对 @IvanMih 答案的微小修改。如果您在使用 WKWebview 时遇到大量空白,这个解决方法对我很有效:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in

    if complete != nil {
      let height = webView.scrollView.contentSize
      print("height of webView is: \(height)")
    }
  })
}

基本上,您不再基于 scrollHeight 计算高度,而是使用 webView.scrollView.contentSize 来计算高度。我相信在显示所有内容且用户无需滚动的情况下,这种方法对静态内容效果会很好,虽然可能存在某些破坏情况。


类型'WKWebView'的值没有成员'scrollView'。 - Andrew_STOP_RU_WAR_IN_UA

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接