如何确定WKWebView的内容大小?

Mar*_*ith 49 objective-c ios ios8 wkwebview

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

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

我尝试过sizeToFit和sizeThatFits但没有成功.这是我的代码,它创建一个WKWebView实例并将其添加到容器scrollview:

// 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];
Run Code Online (Sandbox Code Playgroud)

这是一个实验性的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));
}
Run Code Online (Sandbox Code Playgroud)

这是生成的输出:

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}
Run Code Online (Sandbox Code Playgroud)

sizeToFit调用无效,sizeThatFits始终返回高度1.

小智 75

我想我已经阅读了关于这个主题的每一个答案,我所拥有的只是解决方案的一部分.大部分时间我都在尝试实现@davew所描述的KVO方法,偶尔会有效,但大部分时间都在WKWebView容器的内容下留下了空白区域.我还实现了@David Beck建议,并将容器高度设为0,从而避免了容器高度大于内容容量时出现问题的可能性.尽管如此,我偶尔会有空白.所以,对我来说,"contentSize"观察者有很多缺陷.我没有很多网络技术的经验,所以我无法回答这个解决方案的问题,但我看到如果我只在控制台中打印高度但不对它做任何事情(例如调整约束),它会跳到某个数字(例如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
            })
        }

        })
}
Run Code Online (Sandbox Code Playgroud)

当然,正确设置约束以使scrollView根据containerHeight约束调整大小非常重要.

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

  • 我已经使用了上面的代码,但只是在我的html字符串中添加了一些元数据之后:<meta name ="viewport"content ="width = device-width,initial-scale = 1"> (17认同)
  • 对于 iOS 13 **document.body.scrollHeight** 不起作用,所以我使用 **document.documentElement.scrollHeight** 及其对我有用 (16认同)
  • 根据[this gist](https://gist.github.com/pkuecuekyan/f70096218a6b969e0249427a7d324f91)和[此答案](https://stackoverflow.com/a),您可能需要使用`document.body.scrollHeight`. /二十五万五千四百八十九分之四千四百○七万七千七百七十七). (4认同)
  • 在我的情况下不起作用,`document.body.offsetHeight`或`document.body.scrollHeight`都给出错误的高度 (3认同)

dav*_*vew 21

您可以使用键值观察(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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这样可以节省JavaScript的使用,让您在所有更改中保持循环.

  • 使用这种好的KVO导致一个长的空白页结束页面的结尾,因为ios 10.3.有没有人有这种情况的解决方案? (11认同)
  • 这有一个缺点:如果内容更改为小于Web视图的高度,则contentSize将与Web视图的框架相同. (5认同)

And*_*huk 13

我最近不得不自己处理这个问题.最后,我正在使用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)
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

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

首先,在加载您的URL之前,您应该设置shouldListenToResizeNotificationfalse.当加载的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)
    }
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,因为我在报告的高度上添加了8,在完成此操作后,我的大小body将会更改,并且通知将再次发布.

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

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

  • `Source2` 事件监听器永远不会被调用,我已经嵌入了 Instagram 帖子,它每次都给我错误的 `source1` 高度 (3认同)
  • 这有效。请记住通过 webView.configuration.userContentController.removeScriptMessageHandler 彻底删除它,否则 Web 视图会保留您的“self”引用,从而导致内存泄漏。 (2认同)

luh*_*iya 11

为我工作

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

  • 此解决方案不保证在调用“didFinish”后加载将以 0.1 结束 (2认同)

Rav*_*mar 7

You can also got content height of WKWebView by evaluateJavaScript.

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

                  }
              }];
}
Run Code Online (Sandbox Code Playgroud)


小智 6

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

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

然后在webview完成加载后,您就可以确定所需的高度

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

   println(webView.scrollView.contentSize.height)

}
Run Code Online (Sandbox Code Playgroud)

  • 谢谢!但在我的情况下,至少didFinishNavigation太快了(我得到0,0的大小).在WKWebView实例完成加载其内容后,我没有看到一种简单的方法来调用,因此我将尝试使用JS - > Native消息来解决该问题.看起来我需要使用WKUserContentController并实现WKScriptMessageHandler协议. (3认同)
  • 这很好,但有点小技巧.您需要等待十分之一秒才能获得实际内容大小. (2认同)

Str*_*bas 6

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

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

我通过使用 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
    }
)
Run Code Online (Sandbox Code Playgroud)

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


Chr*_*han 5

请尝试以下方法.无论您在何处实例化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]];
Run Code Online (Sandbox Code Playgroud)

然后添加一个类似于以下类的方法,该类遵循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;}
Run Code Online (Sandbox Code Playgroud)

这适合我.

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

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

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


小智 5

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

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

他的评论如下:

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

他的代码如下:

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

}
Run Code Online (Sandbox Code Playgroud)