PIXEL
DOCK

I like the smell of Swift in the morning…

Set the height of a UIWebView to the height of it’s HTML content

Posted: | Author: | Filed under: iOS, Objective-C | Tags: , | 19 Comments »

Sometimes you need to know the height of a html document that is loaded into a UIWebView. For example if you want to set the height of the UIWebView to the height of its content.

The logical way would be this:

1. add a Javascript function to the HTML that returns the height of the document.
2. add the call to this Javascript function to the UIWebViewDelegate’s method webViewDidFinishLoad:

This sounds easy, but if you look at the results you’ll realize that the values for the document height are not correct and pretty random.

The problem is: webViewDidFinishLoad: get’s called when the HTML is fully loaded BUT it still has to be fully rendered before you can determine its height!

So the call comes too early. You could delay the call but that’s not the way to go here as this is still unreliable and you want the height as soon as possible.

The solution is to revert the process. Instead of Objective-C asking the Javascript for the height, have the Javascript call Objective-C as soon as it knows the height of the document.

Here’s how to do it:

1. Add a Javascript function to the HTML document

Add this function either to the head or the body of your HTML

This function gets called as soon as the HTML document is fully rendered. It puts the height into an URL and sends a request with this URL.
If you are asking yourself why I use “ready://” instead of “http://”: I do this because sending a request is the only way, how the Javascript inside a UIWebView can send messages to the UIWebViewDelegate. So this is not a “real” HTTP Request. Instead you can use the URL scheme to make things easier on the Objective-C part (as you will see in step 2).

2. Intercept the request in the UIWebView’s delegate

A UIWebViewDelegate has a method, that get’s called everytime the HTML inside the UIWebView sends a request. Here’s what to do in this method:

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType { 
  NSURL *url = [request URL]; 
  if (navigationType == UIWebViewNavigationTypeOther) { 
    if ([[url scheme] isEqualToString:@"ready"]) { 
      float contentHeight = [[url host] floatValue]; 
      CGRect fr = webview.frame; 
      fr.size = CGSizeMake(webview.frame.size.width, contentHeight); 
      webview.frame = fr; 
      return NO; 
    } 
  }
  return YES; 
}

Here you can see why I used the custom URL scheme “ready”. It makes it easy to identify my request. The URL that the javascript requested has the format “ready://1200” meaning that the HTML content is 1200px high. So I use the scheme “ready” to identify my request and the “host” part of the URL to sent the actual height. Then it’s easy to set the height of the UIWebView to the height of the HTML document’s content.

Return NO to stop the UIWebView from trying to load the request. Don’t forget to return YES for all other requests or the HTML content won’t even get loaded into the UIWebView in the first place.

19 Comments

  • 1

    Justin Shinsaid at

    check out the method (void) sizeToFit.

    I get the desired result by calling in -(void)webViewDidFinishLoad:

    [self.webView sizeToFit]

  • 2

    adminsaid at

    Hi Justin,

    thanks for your suggestion. The way you describe would indeed be the easiest way. However, it might work sometimes but it is not reliable, because -(void)webViewDidFinishLoad gets called when the HTML is loaded. But that does not mean that it is fully rendered by the UIWebView. You might run into a timing bug, when you call sizeToFit on the UIWebView before the HTML is really rendered, because before that the UIWebView just does not know how height it’s content will be. To set the content height in a reliable way, you have to wait until the HTLM is fully rendered. And the only way I found how to do that is use javascript inside the HTML that calls the UIWebViewDelegate when the HTML is ready.

  • 3

    Ernesto Carriónsaid at

    Hi, I’ve tried this solution but If I change the html several times.. the height is always the max height of all calculated heights, have you experienced that?

  • 4

    adminsaid at

    Hi Ernesto,

    thanks for your feedback. No, I’ve not experienced that. It is a bit difficult to see what the problem might be in your case. Can you describe a bit more detailed what you change in your HTML and what the result is? Is the UIWebViewDelegate method called every time you change your HTML?

  • 5

    Millysaid at

    Nice work, this really helped me out. Of note, your javascript code is missing a } at the end of the function, wouldn’t work until I figured that out. I guess that is what happens when you copy-paste! Thanks!

  • 6

    Jörnsaid at

    Hi Milly,

    you are absolutely right! Thanks for pointing that out!

  • 7

    chliulsaid at

    hi~

    your solution still have the timing problem, I have a pageviewcontorller, which include a lot of webview page, the pageviewcontroller will preload the web view, the callbak onload get called.
    but the page is still not rendered.

  • 8

    Jörnsaid at

    Hi chiliul,

    are you using AJAX to add content to your webview or are you using custom fonts, that have to be loaded from a server? That would explain, why onLoad gets called before the HTML is fully displayed.

    I remember I also had that problem once and I had to use a really dirty hack to make it work: In the onLoad event handler I added an interval that would check the body’s offsetHeight every 500ms and would wait until this value did not change anymore before making the request to the UIWebView delegate. Really ugly solution.

    I really hope that using WebViews will get a lot more stable with iOS8’s introduction of WKWebView.

    Best regards,

    Jörn

  • 9

    mattsaid at

    works great. thanks!

  • 10

    Oz Solomonsaid at

    It is better to use document.body.scrollHeight than document.body.offsetHeight since scrollHeight will take margins into account.

  • 11

    Jörnsaid at

    Hi Oz,

    thanks for your feedback, I really appreciate that!
    I got curious and made some tests. And you are right, scrollHeight takes margins into account, but there is a problem when the document height is less than the height of the webview. Then you’ll get the height of the webview and not the height of the actual document.

    Best regards,

    Jörn

  • 12

    El Severosaid at

    Hi there,

    Thanks for the tip, it helped me to achieve most of my goal. I’m still facing an impediment like:

    I have the following structure:

    UIView
    -UIScrollView
    –UIView-ContentView (zero margins to the UIScrollview and equal height to UIScrollVIew’s parent)
    —UIView (fixed height, 0 top margin to Content View)
    —UIView (fixed height, 0 top margin to above view)
    —UIView (fixed height, 0 top margin to above view)
    —UIView (fixed height, 0 top margin to above view)
    —UIWebView (fixed height & linked to IBOutlet in the class)
    —UILabel (top margin to UIWebView)
    —UIView (fixed height, top margin to above label)
    —UIView (fixed height, 0 top margin to above view)
    —UIView (fixed height, 0 top margin to above view)
    —UIView (fixed height, 0 top margin to above view)
    —UIButton (fixed height, top margin to above view)

    My impediment and question in the same time would be how do I’d get the total height of my ContentView? (already tried: CGRectGetHeight(self.contentView.frame) )

    Scenario:
    1. On viewDidLoad() I’m adding the HTML text by appending the JavaScript code that returns the height of the UIWebView
    2. on UIWebViewDelegate method (shouldStartLoadWithRequest) I’m setting the following things:
    – webViewHeight.constant (NSLayoutConstraint, linked from IB)
    – self.scrollView.contentSize = CGSizeMake(self.view.frame.width, CGRectGetHeight(self.contentView.frame))

    The issue is that CGRectGetHeight(self.contentView.frame) doesn’t gets the desired height. Any ideas?

    Looking forward,
    El

  • 13

    Jörnsaid at

    Hi El,

    when you are not working with Auto Layout YOU are the one who is responsible for setting the height on your UIScrollView’s contentView. The UIScrollView has no way of knowing the size of its contentView.

    To make this work you have to add the height of all the subviews and set that height to the UIScrollView’s contentView.

    If you would be using Auto Layout this would work completely different. With Auto Layout you would only have to make sure that all your subviews have constraints between them and that the first subview has a top constraint to the UIScrollView (not the contentView) and the last subview has a bottom constraint to the bottom of the UIScrollView (again not the contentView but the UIScrollView itself). When you do that the UIScrollView can calculate the size of its contentView itself.

    If you are considering using Auto Layout I wrote another blogpost which can show you in more detail how to do it: http://www.pixeldock.com/blog/uiscrollview-and-auto-layout/

    Hope I could help,

    Jörn

  • 14

    El Severosaid at

    Hi there Jörn,

    Thanks for your feedback. I haven’t seen your article at this but I’ve figure it out (based on AutoLayout suggestions).
    The only thing that I still have problem with is when I have a large HTML loaded into my UIWebView and I rotate the device.

    The calculated height of the UIWebView is not calculated properly, the frame of it its smaller (in height) by ~20%.

    Do you have any clue what might be?
    (here’s the code: https://goo.gl/RNfRe3 how I’m setting UIWebView’s height via NSLayoutConstraint).

    Thanks again!

  • 15

    Jörnsaid at

    Hi El,

    when you are rotating the device the height of the web page changes (because the screen is now wider or narrower than before the rotation). This happens without reloading the HTML, so the UIWebView is not aware of it. That’s why the UIWebView’s height is not correct after the roatation.

    I can think of two ways to fix this:

    1. You recalculate the WebView’s height constaint’s constant in your UIViewController’s viewWillTransitionToSize:withTransitionCoordinator: method. That method gets called when you rotate the device.

    2. You observe the contentSize property of the UIScrollView inside the UIWebView using KVO. Have a look at this article to see how it can be done: http://www.pixeldock.com/blog/set-a-uiwebviews-height-to-the-height-of-its-html-content-using-auto-layout/

    Hope that helps,

    Jörn

  • 16

    El Severosaid at

    Hi,

    Regarding to #1 way I’m not sure how to do it, Only if you’d elaborate more (if you don’t mind).

    I’ve applied your great article to my issue but still not there 🙁 the problem that still persists on my end is that the contentSize height of the UIWebView is way more higher than its actual size.

    There should be something wrong with the JavaScript code that returns me the content height.

    A small output from my debugging session:

    8028.0 <— First time called
    11040.0 <— 2nd time called
    ###### after rotated #####
    8028.0 <— first call
    11040.0 <— 2nd call

    here's the html content that I'm loading into my UIWebView (https://gist.github.com/alexszilagyi/4871d007afc36ef99d9566fc31479f5f#gistcomment-1745265).

    Based on my all tryouts and all things that I'm thinking of would be that after rotation when the JavaScript returns the height it doesn't returns the correct value, and that incorrect values is passed only to the NSLayoutConstraint and makes my UIWebView higher that its actual size.

    P.S. – Very nice article, +1! 😉 too bad that I've missed out at first and couldn't used your get there in first place.

    Thanks again!

  • 17

    El Severosaid at

    P.S. – I’ve also have a small playground for my issue (after updating the layout based on your article) (https://www.dropbox.com/s/g7jv63lbkmbtja8/UIScrollViewIssue.zip?dl=1). I’m facing also in this project an issue with rotation but ignore, I’ll fix it later (already fixed in my final project).

  • 18

    Jörnsaid at

    Hi El,

    I had a quick look at your code. You are using both the Javascript method and the KVO method. When you are using the KVO method you do not need the Javascript method and should remove it.

    Best regards,

    Jörn

  • 19

    JPsaid at

    Very useful, if using WKWebView after catching the call in the delegate you can use the scroll view content to get the size.


Leave a Reply