如何确定WKWebView的内容大小?

时间:2014-12-16 22:34:36

标签: ios objective-c 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];

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

18 个答案:

答案 0 :(得分:55)

我想我已经阅读了关于这个主题的每一个答案,我所拥有的只是解决方案的一部分。大部分时间我都在尝试实现@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
            })
        }

        })
}

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

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

答案 1 :(得分:21)

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

在ViewController中:

overflow:visible;

这将节省JavaScript的使用,让您在所有更改中保持循环。

答案 2 :(得分:9)

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

    }
}

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

首先,在加载您的网址之前,您应该将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的大小会发生变化,通知会再次发布。

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

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

答案 3 :(得分: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]];

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

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

答案 4 :(得分:4)

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

WKWebView Content loaded function never get called

然后在webview完成加载后,您可以通过

确定所需的高度
func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) {

   println(webView.scrollView.contentSize.height)

}

答案 5 :(得分:2)

使用@Andriy的回答和this answer我能够在WKWebView中设置获取contentSize的高度并改变它的高度。

这里是完整的快速4代码:

    var neededConstraints: [NSLayoutConstraint] = []

    @IBOutlet weak var webViewContainer: UIView!
    @IBOutlet weak var webViewHeight: NSLayoutConstraint! {
        didSet {
            if oldValue != nil, oldValue.constant != webViewHeight.constant {
                view.layoutIfNeeded()
            }
        }
    }


   lazy var webView: WKWebView = {
        var source = """
var observeDOM = (function(){
    var MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
        eventListenerSupported = window.addEventListener;

    return function(obj, callback){
        if( MutationObserver ){
            // define a new observer
            var obs = new MutationObserver(function(mutations, observer){
                if( mutations[0].addedNodes.length || mutations[0].removedNodes.length )
                    callback();
            });
            // have the observer observe foo for changes in children
            obs.observe( obj, { childList:true, subtree:true });
        }
        else if( eventListenerSupported ){
            obj.addEventListener('DOMNodeInserted', callback, false);
            obj.addEventListener('DOMNodeRemoved', callback, false);
        }
    };
})();

// Observe a specific DOM element:
observeDOM( document.body ,function(){
    window.webkit.messageHandlers.sizeNotification.postMessage({'scrollHeight': document.body.scrollHeight,'offsetHeight':document.body.offsetHeight,'clientHeight':document.body.clientHeight});
});

"""

        let script = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
        let controller = WKUserContentController()
        controller.addUserScript(script)
        controller.add(self, name: "sizeNotification")
        let configuration = WKWebViewConfiguration()
        configuration.userContentController = controller
        let this = WKWebView(frame: .zero, configuration: configuration)
        webViewContainer.addSubview(this)
        this.translatesAutoresizingMaskIntoConstraints = false
        this.scrollView.isScrollEnabled = false
        // constraint for webview when added to it's superview
        neededConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[web]|",
                                                            options: [],
                                                            metrics: nil,
                                                            views: ["web": this])
        neededConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[web]|",
                                                            options: [],
                                                            metrics: nil,
                                                            views: ["web": this])
        return this
    }()


    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        _  = webView // to create constraints needed for webView
        NSLayoutConstraint.activate(neededConstraints)
        let url = URL(string: "https://www.awwwards.com/")!
        let request = URLRequest(url: url)
        webView.load(request)
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if let body = message.body as? Dictionary<String, CGFloat>,
            let scrollHeight = body["scrollHeight"],
            let offsetHeight = body["offsetHeight"],
            let clientHeight = body["clientHeight"] {
            webViewHeight.constant = scrollHeight
            print(scrollHeight, offsetHeight, clientHeight)
        }
    }

答案 6 :(得分:2)

大多数答案都使用“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
    }
)

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

答案 7 :(得分:2)

为我工作

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

答案 8 :(得分:2)

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

                  }
              }];
}

答案 9 :(得分:1)

我已经尝试过滚动视图KVO,我尝试使用clientHeightoffsetHeight等来评估文档上的javascript ...

最终对我有用的是:document.body.scrollHeight。或者使用最顶层元素的scrollHeight,例如容器div

我使用KVO听取loading WKWebview属性更改:

[webview addObserver: self forKeyPath: NSStringFromSelector(@selector(loading)) options: NSKeyValueObservingOptionNew context: nil];

然后:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if(object == self.webview && [keyPath isEqualToString: NSStringFromSelector(@selector(loading))]) {
        NSNumber *newValue = change[NSKeyValueChangeNewKey];
        if(![newValue boolValue]) {
            [self updateWebviewFrame];
        }
    }
}

updateWebviewFrame实施:

[self.webview evaluateJavaScript: @"document.body.scrollHeight" completionHandler: ^(id response, NSError *error) {
     CGRect frame = self.webview.frame;
     frame.size.height = [response floatValue];
     self.webview.frame = frame;
}];

答案 10 :(得分:1)

我发现hlung在这里对WKWebView进行如下扩展是我最简单,最有效的解决方案:

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

他的评论如下:

“很好!对我来说,我没有设置webView.frame,而是设置了自动布局内在ContentSize。”

他的代码如下:

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

}

答案 11 :(得分:0)

我想为上述答案中未提及的特殊情况提供解决方案,如果您在 WKWebView 中使用自定义字体,可能会发生这种情况。

我尝试了此处解释的所有解决方案,以及其他 StackOverflow 问题中提到的许多其他解决方案。没有什么对我来说是 100% 正确的。我总是遇到同样的问题:返回的高度总是比 WkWebView 的实际高度小一点。尝试过WKNavigationDelegate方式,尝试通过在渲染的HTML中注入js来监听自生成事件,没有成功,高度总是出错。

我学到的第一件事:在加载 html 并等待完成的事件之前,必须将 webview 添加到布局中。如果你试图以孤立的方式渲染 webview 而没有将其添加到布局中,那么高度就会非常错误。

奇怪的是,我发现在html渲染后设置断点,调用高度评估方法之前,返回的高度是正确的。测量哪个高度(scrollHeight 或 offsetheight)并不重要,两者总是正确的。

这为我指明了正确的方向。结论很明显(虽然我需要很多天进行调试才能实现):收到didFinishNavigation事件后,或者如果您使用自定义js并监听window.onload事件或类似事件,返回的高度几乎是正确的但不完全是因为渲染还没有完成。

here所述,Firefox、Chrome 和 Safari 在将字体应用到文档之前触发 DomContenLoaded 事件(也许,在将 css 应用到文档之前也?)。就我而言,我使用的是嵌入在我的应用程序中并以经典方式在 HTML 中引用的自定义字体:

 <style>
    @font-face {
        font-family: 'CustomFont';
        src: url('montserrat.ttf');
        format('truetype');
    }

    body{
        font-family: 'CustomFont';
        font-size: 12px;
    }

解决方案? 您必须收听在 window.onload 事件之后发生的事件 document.fonts.ready 等。在 WkWebView 中加载的 html 中嵌入以下 js:

    document.fonts.ready.then(function() {
 window.webkit.messageHandlers.iosEventListener.postMessage('custom_event_fonts_ready');
  
});

然后在您的 iOS 应用程序中,使用

收听事件
  self.webView.configuration.userContentController.add(self, name: "iosEventListener")

收到时

        public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            if let body = message.body as? String {
                if (body == "custom_event_fonts_ready") {
                        self.evaluateBodyHeight()
    }
            }
        }

 private func evaluateBodyHeight() {
        self.webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
            if complete != nil {
                self.webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
                    let webViewHeight = height as! CGFloat
//Do something with the height.


                })

            }
        })

    }

我不确定,但我认为使用此解决方案,测量 Web 视图高度的所有不同方法都会返回正确的方法。经过近一个月的调试和绝望,我不想测试它们

为我的英语不好道歉。

答案 12 :(得分:0)

对于Webkit中的任何内容,以下代码对我而言都是完美的。确保将以下委托添加到您的类中:WKNavigationDelegate。

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        self.bodyWebView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
            if complete != nil {
                self.bodyWebView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
                    let heightWebView = height as! CGFloat
                    //heightWebView is the height of the web view
                })
            }
        })
    }
}

调度非常重要,因为这样可以确保在加载Web视图结束时获得的高度正确,这是由于html可能具有的元素类型而引起的。

答案 13 :(得分:0)

也尝试实现不同的方法,最终找到了解决方案。结果,我制作了一个自动调整大小的WKWebView,使它的 intrinsicContentSize 适应其内容的大小。因此,您可以在自动版式中使用它。举例来说,我做了一个视图,它可以帮助您在iOS应用上显示数学公式:https://github.com/Mazorati/SVLatexView

答案 14 :(得分:0)

这是@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来计算高度。我敢肯定在某些情况下这种情况会中断,但是我认为它对于静态内容以及如果您要显示所有内容而无需用户滚动就可以很好地解决问题。

答案 15 :(得分:0)

我在UITableViewCell中尝试了Javascript版本,并且运行良好。但是,如果要将其放在scrollView中。我不知道为什么,高度可以更高,但不能更短。但是,我在这里找到了UIWebView解决方案。 https://stackoverflow.com/a/48887971/5514452

它也可以在WKWebView中使用。我认为问题是因为WebView需要重新布局,但不知何故它不会缩小而只能放大。我们需要重置高度,它肯定会调整大小。

编辑:我在设置约束后重置了框架高度,因为有时由于将框架高度设置为0而无法使用。

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

答案 16 :(得分:0)

你需要添加延迟,它适合我,而不是上面的JS解决方案:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
        print(size: webView.scrollView.contentSize)
    })
}

答案 17 :(得分:-1)

经过大量的实验,我设法找到了一个对我有用的解决方案,我发现无需使用javascript就能使webview高动态化,并且无需从webview中获取高度常数就可以像我的魅力一样与我一起工作,并且在我向HTML注入新样式,并使用字体大小和高度

Swift中的代码

1-给您的Webview导航代表

  webView.navigationDelegate = self

2-委托扩展

extension yourclass : WKNavigationDelegate {
      func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        // Handel Dynamic Height For Webview Loads with HTML
       // Most important to reset webview height to any desired height i prefer 1 or 0  
        webView.frame.size.height = 1
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        // here get height constant and assign new height in it 
            if let constraint = (webView.constraints.filter{$0.firstAttribute == .height}.first) {
                constraint.constant = webView.scrollView.contentSize.height
            }
 }

希望它也适用于你们 **请注意,这并不是我在StackOverflow和其他站点上进行大量搜索所花费的全部精力,这最终也可以通过大量测试与我合作