86

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.

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

I have tried sizeToFit and sizeThatFits without success. Here is my code that creates a WKWebView instance and adds it to the container 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];

Here is an experimental didFinishNavigation method:

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

Mark Smith
  • 993
  • 1
  • 7
  • 7
  • Update: I am still in search of a solution. If I load remote content via [self.mWebView loadRequest:req]), the size is available via self.mWebView.scrollView.contentSize inside didFinishNavigation. But if I load my content via [self.mWebView loadHTMLString:s], the size is not available until sometime later. Using loadRequest with a dataURL does not solve the problem. And I do not know when 'later' is. – Mark Smith Dec 18 '14 at 16:27
  • 1
    I wonder if there could ever be a meaningful answer to this? Consider that at any time a webpage might size its contents to the size of its window, so how could it be that we might change the size of the window to the size of its contents? I think there is a reason WKWebView doesn't provide an intrinsicContentSize, there is simply no single, meaningful 'intrinsic' size. – wardw Oct 20 '17 at 08:37
  • year 2021: addUserScript then use a ResizeObserver. – dklt Feb 01 '21 at 09:57

20 Answers20

147

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.

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.

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

mfaani
  • 33,269
  • 19
  • 164
  • 293
IvanMih
  • 1,815
  • 1
  • 11
  • 23
  • I finally tried again after seeing this answer, and it worked for me. – Mark Smith Nov 17 '17 at 17:25
  • 6
    You may want to use `document.body.scrollHeight` instead, as per [this gist](https://gist.github.com/pkuecuekyan/f70096218a6b969e0249427a7d324f91) and [this answer](https://stackoverflow.com/a/44077777/255489). – Zev Eisenberg Dec 29 '17 at 19:05
  • 5
    didn't work in my case, either `document.body.offsetHeight` or `document.body.scrollHeight` both giving wrong height – Ratul Sharker May 17 '18 at 05:52
  • 46
    I've made it work with the code above, but only after adding some metadata to my html string: – andrei Aug 30 '18 at 14:42
  • @andrei I've tried most of these KVO/javascript evaluation solutions. I've finally made it work with this tag. – ZShock Oct 24 '18 at 18:56
  • 1
    For me `WKWebView` document.body.offsetHeight returns wrong height, any idea why? https://stackoverflow.com/questions/54187194/wkwebview-document-body-offsetheight-returns-wrong-height-why – János Jan 14 '19 at 18:35
  • I have figured out that using the KVO is more correct when you turn off `-webkit-overflow-scrolling` and `overflow` in CSS of the Web. – Huy Le Jan 21 '19 at 06:44
  • I am using the WKWebView in a UIPageViewController, and the only thing that helped me was setting the initial frame in the viewDidLayoutSubviews and than load the html. The only thing to remember here is that the viewDidLayoutSubviews is called multiple times so you'll have to set a boolean of some sort so you won't do it twice – Vasco Feb 25 '19 at 12:11
  • The `completionHandler` of `document.readyState` awaits a long time if the web page content is too long, I prefer to combine this with @davew 's KVO solution. – Itachi Apr 23 '19 at 08:15
  • This works pretty nicely when loading html content, but fails when loading a pdf. the height value comes out to be nil every time. Anyone know the pdf counterpart to this code? – KarmaDeli Sep 24 '19 at 17:38
  • 23
    For iOS 13 **document.body.scrollHeight** not working so i use **document.documentElement.scrollHeight** and its working for me – Chirag Kothiya Nov 13 '19 at 13:27
  • readyState should have "complete" value : https://developer.mozilla.org/fr/docs/Web/API/Document/readyState – vmeyer Feb 12 '20 at 15:07
  • link's in `html` are no more click able after setting the height of `self.webKitHeightCons.constant` ... – Wahab Khan Jadon Feb 16 '20 at 11:51
  • For iOS 12 **document.body.scrollHeight** and **document.documentElement.scrollHeight** are both not working. Are there any solutions to this? – Tim Langner Jun 07 '22 at 14:02
29

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

In your 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.

davew
  • 1,255
  • 15
  • 27
  • 5
    There is one downside to this: if the content changes to be smaller than the height of the web view, the contentSize will be the same as the web view's frame. – David Beck Aug 27 '16 at 03:57
  • 11
    using this kind ok KVO resulting a long blank page end the end of a page since ios 10.3. does anyone has a solution for this situation? – Raditya Kurnianto May 15 '17 at 07:26
22

I had to deal with this issue myself recently. In the end, I was using a modification of the solution proposed by 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-height and max-width attributes.

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

Monitor resizing of the 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 shouldListenToResizeNotification to 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.

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 WKWebView to anything other than the size reported by the notification - the notification will be triggered again.

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 body will change and the notification will be posted again.

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!

Community
  • 1
  • 1
Andriy Gordiychuk
  • 6,163
  • 1
  • 24
  • 59
  • Andriy, I cannot thank you more. This would've been saved me several hours of Googling and trying. Just a little additional info: this worked me with the `meta viewport` tag set to `width=device-width, initial-scale=1.0, shrink-to-fit=no` and I removed the constraint setup under `shouldListenToResizeNotification = true`. – Tamás Sengel Jun 21 '17 at 17:45
  • I was also able to get this to work, using this approach. Thank you!. Just curious though, if anyone has tried the webview.scrollView.addObserver approach documented here? https://stackoverflow.com/a/33289730/30363 – Blue Waters Jan 07 '19 at 03:46
  • 2
    This works. Remember to remove it cleanly via webView.configuration.userContentController.removeScriptMessageHandler otherwise the web view holds on to your `self` reference resulting in a memory leak. – Kunal Dec 30 '19 at 06:42
  • 3
    the `Source2 ` event listener is never called, I've en embed instagram post and it give me wrong height every time for `source1` – Chlebta Mar 27 '20 at 12:22
  • I'm seeing the same issue as Chlebta - this doesn't work for pages who resize because of a social embed. – Zack Nov 09 '20 at 17:12
9

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
        }
    }
}
luhuiya
  • 2,129
  • 21
  • 20
8

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

                  }
              }];
}
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Ravi Kumar
  • 1,356
  • 14
  • 22
7

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

    //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:

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

@"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.

  • In `userContentController:didReceiveScriptMessage:` I receive empty dictionary. :( – Timur Bernikovich Oct 21 '16 at 16:52
  • It only works if you change `document.width` and `document.height` to `window.innerWidth` and `window.innerHeight`, that's why you were getting an empty dictionary, @TimurBernikowich. – paulvs Nov 24 '16 at 19:35
  • 1
    Don't know much about javascript but `document.body.scrollHeight` worked for me to get an accurate height. `document.height` and `window.innerHeight` were both 0. `window.onload=function () {...}` was also necessary. – Ryan Poolos Jan 31 '17 at 22:57
7

Most answers are using "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:

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.

Stratubas
  • 2,939
  • 1
  • 13
  • 18
  • 1
    +1 for the JS here! It handles one very important edge case! It correctly measures webview height, for a webview that was previously shorter than its content (e.g. had a vertical scrollbar) but has now become taller. This happens when rotating a device from portrait to landscape and your webview suddenly becomes wider. In this case the document.body.scrollOffset returns the previous (higher) value, even though the content is now shorter (due to the increased width) and leaves a lot of whitespace at the bottom. I don't like the KVO approach though. The didFinish delegate method is much cleaner. – m_katsifarakis Feb 26 '19 at 12:29
7

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

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

His comment follows:

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

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

}
Graeme Campbell
  • 364
  • 2
  • 4
6

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

WKWebView Content loaded function never get called

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

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

   println(webView.scrollView.contentSize.height)

}
Community
  • 1
  • 1
  • 3
    Thanks! But in my case at least didFinishNavigation is too soon ( I get 0, 0 for the size). I do not see a simple way to get called after a WKWebView instance has finished loading its content, so I will try using a JS -> Native message to solve that problem. It looks like I will need to use WKUserContentController and also implement the WKScriptMessageHandler protocol. – Mark Smith Dec 17 '14 at 20:38
  • 3
    This works fine but with little trick. You need to wait another tenth of a second to get actual content size. – Borut Tomazin Feb 05 '16 at 14:17
  • 1
    Nah, that solution isn't right. I had 0.1 delay, but in some cases that wasn't enough. If you have more content you have to increase delay again and again. – Makalele Mar 13 '18 at 11:06
3

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

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 scrollHeight you 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.

Ronak Vora
  • 185
  • 3
  • 16
3

After lots of experiments I've managed to find a solution that works for me I found to make a webview heigh dynamic without using evaluating javascript and also without taking height constant from webview this work with me like a charm and also work when I inject new style to HTML and play with font sizes and heights

code in Swift

1- give your Webview navigation delegate

  webView.navigationDelegate = self

2- in delegation extension

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

hope it works also with you guys ** note this not my entire effort I searched a lot in StackOverflow and other sites and this is what finally works with me with a lot of testing also

2

using @Andriy's answer and this answer i was able to set get height of contentSize in WKWebView and change it's height.

here is full swift 4 code:

    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)
        }
    }
Mohammadalijf
  • 1,387
  • 9
  • 19
1

I've tried the scroll view KVO and I've tried evaluating javascript on the document, using clientHeight, offsetHeight, etc...

What worked for me eventually is: document.body.scrollHeight. Or use the scrollHeight of your top most element, e.g. a container div.

I listen to the loading WKWebview property changes using KVO:

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

And then:

- (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];
        }
    }
}

The updateWebviewFrame implementation:

[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;
}];
natanavra
  • 2,100
  • 3
  • 18
  • 24
1

I tried Javascript version in UITableViewCell, and it works perfectly. However, if you want to put it in the scrollView. I don't know why, the height can be higher but cannot be shorter. However, I found a UIWebView solution here. https://stackoverflow.com/a/48887971/5514452

It also works in WKWebView. I think the problem is because the WebView need relayout, but somehow it will not shrink and can only enlarge. We need to reset the height and it will definitely resize.

Edit: I reset the frame height after setting the constraint because sometime it will not working due to setting the frame height to 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
            })
        }
    })
}
Jeff Zhang
  • 140
  • 1
  • 7
  • Of the gazillion solutions I tried, `self.webView.frame.size.height = 0` is the one and only that worked –  May 22 '20 at 21:13
1

Also tried to implement different methods and finally came to a solution. As a result I made a self-sizing WKWebView, that adapts its intrinsicContentSize to the size of its contents. So you can use it in Auto Layouts. As an example I made a view, which might help you display math formula on iOS apps: https://github.com/Mazorati/SVLatexView

mazy
  • 652
  • 1
  • 10
  • 18
1

The following code has worked perfectly for me, for any content in the webkit. Make sure to add the following delegate to your class: 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
                })
            }
        })
    }
}

The dispatch is important, because this way you ensure that the height obtained at the end of loading the web view is correct, this happens because of the type of elements that the html may have.

Surckarter
  • 1,660
  • 1
  • 10
  • 7
0

I want to contribute with the solution for a special case that is not mentioned in the answers above and that may happen to you if you are using custom fonts in your WKWebView.

I tried ALL the solutions explained here, and many others mentioned in other StackOverflow questions. Nothing was working 100% correctly for me. I had always the same problem: the height returned was always a little smaller than the real height of the WkWebView. I tried WKNavigationDelegate way, and I tried to listen to self-generated events by injecting js into the HTML rendered, without success, the height was always wrong in all cases.

The first thing I learned: the webview has to be added to the layout before loading the html and waiting for the finished event. If you try to render the webview in an isolated way without adding it before to the layout, then the height will be very wrong.

Curiously, I found out that setting a breakpoint after the html was rendered, and before calling the height evaluation method, then the returned height was correct. It was not important which height was measured (scrollHeight or offsetheight), both were always correct.

That pointed me in the right direction. The conclusion was obvious (although I needed a lot of days making debug to realize it): after the didFinishNavigation event is received, or if you are using custom js and listening to the window.onload event or similar, the height returned is almost correct but not completely because the rendering is not finished yet.

As explained here, Firefox, Chrome, and Safari trigger the DomContenLoaded event before the font-face is applied to the document (and maybe, before the css is applied to the document too?). In my case, I was using a custom font embedded in my app and referenced in the HTML in the classical way:

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

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

Solution? You have to listen to the event document.fonts.ready, that happens after the event window.onload and the like. Embeed the following js in the html you are loading in the WkWebView:

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

Then in your iOS app, listen to the event with

  self.webView.configuration.userContentController.add(self, name: "iosEventListener")

and when received

        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.


                })

            }
        })

    }

I'm not sure, but I think that with this solution, all the different ways to measure the height of web view will return the correct one. After almost one month of debugging and being desperate, I have no desire to test them

Apologizes for my bad English.

Dharman
  • 30,962
  • 25
  • 85
  • 135
R. Campos
  • 947
  • 1
  • 10
  • 26
0

The best way is to observe contentSize property of webView.scrollView and update height constraint of webView accordingly:

private var contentSizeObserver: NSKeyValueObservation?
    
contentSizeObserver = webView.scrollView.observe(\.contentSize, options: .new) { [weak self] _, change in
    guard let contentSize = change.newValue else { return }
    self?.csWebViewHeight?.update(offset: contentSize.height)
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    // Recalculate webView size
    csWebViewHeight?.update(offset: 0)
    webView.setNeedsLayout()
    webView.layoutIfNeeded()
}
Ace Rodstin
  • 132
  • 1
  • 6
0

None of the listed JS funcs to get the content height worked reliably for me. What I found to consistently work is to find the last element in the DOM and get its position explicitly:

webView.evaluateJavaScript(
    "document.body.lastChild.getBoundingClientRect().bottom + window.scrollY"
) { [weak self] (result, _) in
    guard let self = self,
        let height = result as? CGFloat,
        height > 0 else { return }
                    
    self.heightConstraint?.constant = height
}
Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
hundreth
  • 841
  • 4
  • 8
0

The code below worked for me, but this maybe go wrong once WKWebview's view tree changes.

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
        if let scroll_cls = NSClassFromString("WKChildScrollView"),
           let compose_cls = NSClassFromString("WKCompositingView") {
            let viewss = webView.allSubViewsOf(cls: scroll_cls)
            if let first = viewss.first?.subviews.first,
               first.isKind(of: compose_cls) {
                if webView.bounds.height >= first.bounds.height {
                    // webivew is at bottom
                }
            }
        }
        
    }
}


extension UIView {
    func allSubViewsOf(cls: AnyClass) -> [UIView] {
        var all = [UIView]()
        func getSubview(view: UIView) {
            if view.isKind(of: cls) {
                all.append(view)
            }
            guard view.subviews.count > 0 else { return }
            view.subviews.forEach{ getSubview(view: $0) }
        }
        getSubview(view: self)
        return all
    }
}
Roy
  • 1
  • 3
  • this is work only on a H5 which is a simple image – Roy Apr 10 '23 at 07:34
  • Remember that Stack Overflow isn't just intended to solve the immediate problem, but also to help future readers find solutions to similar problems, which requires understanding the underlying code. This is especially important for members of our community who are beginners, and not familiar with the syntax. Given that, **can you [edit] your answer to include an explanation of what you're doing** and why you believe it is the best approach? – Jeremy Caney Apr 19 '23 at 00:28