ios 如何确定 WKWebView 的内容大小?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/27515236/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 04:03:22  来源:igfitidea点击:

How to determine the content size of a WKWebView?

iosobjective-cios8wkwebview

提问by Mark Smith

I am experimenting with replacing a dynamically allocated instance of UIWebView with a WKWebView instance when running under iOS 8 and newer, and I cannot find a way to determine the content size of a WKWebView.

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

My web view is embedded within a larger UIScrollView container, and therefore I need to determine the ideal size for the web view. This will allow me to modify its frame to show all of its HTML content without the need to scroll within the web view, and I will be able to set the correct height for the scroll view container (by setting scrollview.contentSize).

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

I have tried sizeToFit and sizeThatFits without success. Here is my code that creates a WKWebView instance and adds it to the container scrollview:

我尝试过 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];

Here is an experimental didFinishNavigation method:

这是一个实验性的 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));
}

And here is the output that is generated:

这是生成的输出:

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}

The sizeToFit call has no effect and sizeThatFits always returns a height of 1.

sizeToFit 调用无效,并且 sizeThatFits 始终返回高度 1。

回答by IvanMih

I think I read every answer on this subject and all I had was part of the solution. Most of the time I spent trying to implement KVO method as described by @davew, which occasionally worked, but most of the time left a white space under the content of a WKWebView container. I also implemented @David Beck suggestion and made the container height to be 0 thus avoiding the possibility that the problem occurs if the container height is larger that that of the content. In spite of that I had that occasional blank space. So, for me, "contentSize" observer had a lot of flaws. I do not have a lot of experience with web technologies so I cannot answer what was the problem with this solution, but i saw that if I only print height in the console but do not do anything with it (eg. resize the constraints), it jumps to some number (e.g. 5000) and than goes to the number before that highest one (e.g. 2500 - which turns out to be the correct one). If I do set the height constraint to the height which I get from "contentSize" it sets itself to the highest number it gets and never gets resized to the correct one - which is, again, mentioned by @David Beck comment.

我想我阅读了关于这个主题的所有答案,而我所拥有的只是解决方案的一部分。大部分时间我都花在尝试实现@davew 所描述的 KVO 方法上,该方法偶尔会奏效,但大部分时间在 WKWebView 容器的内容下留下空白。我还实现了@David Beck 的建议,并将容器高度设为 0,从而避免了容器高度大于内容高度时出现问题的可能性。尽管如此,我还是偶尔有空白。所以,对我来说,“contentSize”观察者有很多缺陷。我对网络技术没有很多经验,所以我无法回答这个解决方案的问题,但我看到如果我只在控制台中打印高度但不做任何事情(例如调整约束的大小),它跳转到某个数字(例如 5000),然后转到最高数字之前的数字(例如 2500 - 结果是正确的数字)。如果我确实将高度约束设置为我从“contentSize”获得的高度,它会将自身设置为它获得的最高数字,并且永远不会调整为正确的数字 - 这再次由@David Beck 评论提到。

After lots of experiments I've managed to find a solution that works for me:

经过大量实验,我设法找到了适合我的解决方案:

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
            })
        }

        })
}

Of course, it is important to set the constraints correctly so that scrollView resizes according to the containerHeight constraint.

当然,正确设置约束很重要,以便 scrollView 根据 containerHeight 约束调整大小。

As it turns out didFinish navigation method never gets called when I wanted, but having set document.readyStatestep, the next one (document.body.offsetHeight) gets called at the right moment, returning me the right number for height.

事实证明 didFinish 导航方法从未在我想要的时候被调用,但是设置了document.readyState步骤后,下一个 ( document.body.offsetHeight) 在正确的时刻被调用,返回正确的高度数字。

回答by davew

You could use Key-Value Observing (KVO)...

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

In your ViewController:

在您的 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);
    }
}

This would save the use of JavaScript and keep you in the loop on all changes.

这将节省 JavaScript 的使用,并使您随时了解所有更改。

回答by Andriy Gordiychuk

I had to deal with this issue myself recently. In the end, I was using a modification of the solution proposed by Chris McClenaghan.

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

Actually, his original solution is pretty good and it works in most simple cases. However, it only worked for me on pages with text. It probably also works on pages with images that have a static height. However, it definitely doesn't work when you have images whose size is defined with max-heightand max-widthattributes.

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

And this is because those elements can get resized afterthe page is loaded. So, actually, the height returned in onLoadwill always be correct. But it will only be correct for that particular instance. The workaround is to monitor the change of the bodyheight and respond to it.

这是因为这些元素可以在页面加载调整大小。所以,实际上,返回的高度onLoad总是正确的。但它只适用于那个特定的实例。解决方法是监控body高度的变化并做出响应。

Monitor resizing of the document.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)
        }

    }
}

This solution is by far the most elegant that I could come up with. There are, however, two things you should be aware of.

这个解决方案是迄今为止我能想到的最优雅的解决方案。但是,您应该注意两件事。

Firstly, before loading your URL you should set shouldListenToResizeNotificationto false. This extra logic is needed for cases when the loaded URL can change rapidly. When this occurs, notifications from old content for some reason can overlap with those from the new content. To prevent such behaviour, I created this variable. It ensures that once we start loading new content we no longer process notification from the old one and we only resume processing of resize notifications after new content is loaded.

首先,在加载您的 URL 之前,您应该设置shouldListenToResizeNotificationfalse. 当加载的 URL 可以快速更改时,需要这个额外的逻辑。发生这种情况时,由于某种原因来自旧内容的通知可能会与来自新内容的通知重叠。为了防止这种行为,我创建了这个变量。它确保一旦我们开始加载新内容,我们就不再处理来自旧内容的通知,并且只有在加载新内容后才继续处理调整大小通知。

Most importantly, however, you need to be aware about this:

但是,最重要的是,您需要了解这一点:

If you adopt this solution you need to take into account that if you change the size of your WKWebViewto anything other than the size reported by the notification - the notification will be triggered again.

如果您采用此解决方案,您需要考虑到如果您将您的尺寸更改WKWebView为通知所报告的尺寸以外的任何尺寸 - 通知将再次触发。

Be careful with this as it is easy to enter an infinite loop. For example, if you decide to handle the notification by making your height equal to reported height + some extra padding:

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

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)
    }

As you can see, because I am adding 8 to the reported height, after this is done the size of my bodywill change and the notification will be posted again.

如您所见,因为我在报告的高度上加了 8,所以在完成此操作后,我的尺寸body会发生变化,并且将再次发布通知。

Be alert to such situations and otherwise you should be fine.

警惕这种情况,否则你应该没事。

And please let me know if you discover any problems with this solution - I am relying on it myself so it is best to know if there are some faults which I haven't spotted!

如果您发现此解决方案有任何问题,请告诉我 - 我自己依赖它,所以最好知道是否有一些我没有发现的错误!

回答by luhuiya

Works for me

为我工作

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

回答by Chris McClenaghan

Try the following. Wherever you instantiate your WKWebView instance, add something similar to the following:

请尝试以下操作。无论您在何处实例化 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]];

Then add a method similar to the following to which ever class obeys WKScriptMessageHandler protocol to handle the message:

然后添加一个类似于以下的方法到哪个类遵循 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;}

This works for me.

这对我有用。

If you have more than text in your document you may need to wrap the javascript like this to ensure everything is loaded:

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

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

NOTE: This solution does not address ongoing updates to the document.

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

回答by Brian Wells

You need to wait for the webview to finish loading. Here is a working example I used

您需要等待 webview 完成加载。这是我使用的一个工作示例

WKWebView Content loaded function never get called

WKWebView 内容加载函数永远不会被调用

Then after webview has finished loading, then you can determine the heights you need by

然后在 webview 加载完成后,然后你可以确定你需要的高度

func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) {

   println(webView.scrollView.contentSize.height)

}

回答by Ravi Kumar

You can also got content height of WKWebView by evaluateJavaScript.

您还可以通过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

                  }
              }];
}

回答by Stratubas

Most answers are using "document.body.offsetHeight".

大多数答案都使用“document.body.offsetHeight”。

This hides the last object of the body.

这隐藏了身体的最后一个对象。

I overcame this issue by using a KVO observer listening for changes in WKWebview "contentSize", then running this code:

我通过使用 KVO 观察器侦听 WKWebview“contentSize”中的更改,然后运行以下代码克服了这个问题:

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
    }
)

It's not the prettiest code possible, but it worked for me.

这不是最漂亮的代码,但它对我有用。

回答by Graeme Campbell

I found that the answer by hlung here, extending the WKWebView as follows was the simplest and most effective solution for me:

我发现 hlung here 的答案,如下扩展 WKWebView 对我来说是最简单和最有效的解决方案:

https://gist.github.com/pkuecuekyan/f70096218a6b969e0249427a7d324f91

https://gist.github.com/pkuecuekyan/f70096218a6b969e0249427a7d324f91

His comment follows:

他的评论如下:

"Nice! For me, instead of setting the webView.frame, I set autolayout intrinsicContentSize."

“很好!对我来说,我没有设置 webView.frame,而是设置了 autolayout internalContentSize。”

And his code was as follows:

他的代码如下:

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()
    })
  }

}

回答by Ronak Vora

This is a slight edit of @IvanMih's answer. For those of you experiencing a large white space at the end of your WKWebviewthis solution worked well for me:

这是对@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)")
    }
  })
}

so basically instead of calculating the height based on scrollHeightyou calculate height using webView.scrollView.contentSize. I'm sure there are scenarios where this will break, but I think it'll do pretty well for static content and if you are displaying all the content without the user having to scroll.

所以基本上不是根据scrollHeight你计算高度使用webView.scrollView.contentSize. 我确信在某些情况下这会中断,但我认为它对于静态内容会做得很好,并且如果您在无需用户滚动的情况下显示所有内容。