How to determine the content size of a WKWebView?

87,736

Solution 1

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.

Solution 2

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.

Solution 3

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!

Solution 4

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

Solution 5

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

                  }
              }];
}
Share:
87,736

Related videos on Youtube

Mark Smith
Author by

Mark Smith

I am an experienced software engineer/architect. I work on Firefox extensions, Chrome extensions, iOS apps, Tor Browser, and more.

Updated on July 05, 2022

Comments

  • Mark Smith
    Mark Smith almost 2 years

    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
      Mark Smith over 9 years
      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.
    • wardw
      wardw over 6 years
      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.
    • dklt
      dklt over 3 years
      year 2021: addUserScript then use a ResizeObserver.
  • Mark Smith
    Mark Smith over 9 years
    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.
  • Borut Tomazin
    Borut Tomazin over 8 years
    This works fine but with little trick. You need to wait another tenth of a second to get actual content size.
  • David Beck
    David Beck almost 8 years
    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.
  • Timur Bernikovich
    Timur Bernikovich over 7 years
    In userContentController:didReceiveScriptMessage: I receive empty dictionary. :(
  • paulvs
    paulvs over 7 years
    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.
  • Ryan Poolos
    Ryan Poolos over 7 years
    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.
  • Raditya Kurnianto
    Raditya Kurnianto about 7 years
    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?
  • Tamás Sengel
    Tamás Sengel about 7 years
    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.
  • Mark Smith
    Mark Smith over 6 years
    I finally tried again after seeing this answer, and it worked for me.
  • Zev Eisenberg
    Zev Eisenberg over 6 years
    You may want to use document.body.scrollHeight instead, as per this gist and this answer.
  • Makalele
    Makalele over 6 years
    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.
  • Ratul Sharker
    Ratul Sharker about 6 years
    didn't work in my case, either document.body.offsetHeight or document.body.scrollHeight both giving wrong height
  • andrei
    andrei almost 6 years
    I've made it work with the code above, but only after adding some metadata to my html string: <meta name="viewport" content="width=device-width, initial-scale=1">
  • ZShock
    ZShock over 5 years
    @andrei I've tried most of these KVO/javascript evaluation solutions. I've finally made it work with this tag.
  • Blue Waters
    Blue Waters over 5 years
    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? stackoverflow.com/a/33289730/30363
  • János
    János over 5 years
    For me WKWebView document.body.offsetHeight returns wrong height, any idea why? stackoverflow.com/questions/54187194/…
  • Huy Le
    Huy Le over 5 years
    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.
  • Vasco
    Vasco over 5 years
    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
  • m_katsifarakis
    m_katsifarakis over 5 years
    +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.
  • Itachi
    Itachi about 5 years
    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.
  • Trzy Gracje
    Trzy Gracje almost 5 years
    This solution does not guarantee that loading will end in 0.1 after 'didFinish' is called
  • KarmaDeli
    KarmaDeli almost 5 years
    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?
  • Chirag Kothiya
    Chirag Kothiya over 4 years
    For iOS 13 document.body.scrollHeight not working so i use document.documentElement.scrollHeight and its working for me
  • Mani murugan
    Mani murugan over 4 years
    Nailed it. Best solution. Thanks buddy.
  • Kunal
    Kunal over 4 years
    This works. Remember to remove it cleanly via webView.configuration.userContentController.removeScriptMess‌​ageHandler otherwise the web view holds on to your self reference resulting in a memory leak.
  • vmeyer
    vmeyer over 4 years
    readyState should have "complete" value : developer.mozilla.org/fr/docs/Web/API/Document/readyState
  • Wahab Khan Jadon
    Wahab Khan Jadon over 4 years
    link's in html are no more click able after setting the height of self.webKitHeightCons.constant ...
  • Chlebta
    Chlebta over 4 years
    the Source2 event listener is never called, I've en embed instagram post and it give me wrong height every time for source1
  • Dani
    Dani about 4 years
    what is container?
  • IvanMih
    IvanMih about 4 years
    @DanielSpringer container is a UIView which holds WKWebView. Container is the one whose height should be resized.
  • Dani
    Dani about 4 years
    @IvanMih I figured that and tried and the height was way too short.
  • Dani
    Dani about 4 years
    If I rotate my iPhone from portrait to landscape then it gives way too much height
  • Dani
    Dani about 4 years
    Of the gazillion solutions I tried, self.webView.frame.size.height = 0 is the one and only that worked
  • Zack
    Zack over 3 years
    I'm seeing the same issue as Chlebta - this doesn't work for pages who resize because of a social embed.
  • Andrew___Pls_Support_UA
    Andrew___Pls_Support_UA over 3 years
    Value of type 'WKWebView' has no member 'scrollView'
  • yogipriyo
    yogipriyo about 3 years
    Dunno why but the "webView.frame.size.height = 1" is my saving grace
  • oğuz
    oğuz almost 3 years
    Changing from .async to asyncAfter(deadline: .now() + 0.1) worked for me. thanks!
  • Tim Langner
    Tim Langner about 2 years
    For iOS 12 document.body.scrollHeight and document.documentElement.scrollHeight are both not working. Are there any solutions to this?