PIXEL
DOCK

I like the smell of Swift in the morning…

Set a UIWebView’s height to the height of its HTML content using Auto Layout

Posted: | Author: | Filed under: iOS, Swift | Tags: , | 11 Comments »

In a previous post I described how to programmatically set the height of a UIWebView to fit the height of its HTML content.

This is a different approach using a Storyboard (and a little code). To make things a little bit more interesting I added an UIView that sits on top of the UIWebView and that should scroll out of view when the user scrolls the UIWebView. To make that happen we need an UIScrollView that contains the UIView and the UIWebView:

The UIView could be something like an iAd that you want to display on top of the web content but that should be scrolled out of the view when the user scrolls the web content.

Here is how to make it work:

  1. Connect the UIWebView from the nib to an outlet in your view controller.
  2. Disable scrolling in the UIWebView.
  3. Set the constraints on the `UIScrollView`, the `UIView` and the `UIWebView`:
    • The UIScrollView needs a top, a bottom, a leading and a trailing constraint to the UIViewController’s view.
    • The UIView needs a top, a leading and a trailing constraint to the UIScrollView. It also needs a width constraint that is equal to the UIScrollView’s width to avoid horizontal scrolling (See this post for an explaination). I also add a height constraint, because I want to have the UIView to have a constant height of 100pt.
    • The UIWebView needs a top constraint to the UIView’s bottom, a leading, a trailing and a bottom constraint to the UIScrollview. It also needs a height constraint that we will later set to the height of the HTML content

    The constraints should look like this:

    0FJCG

  4. Connect the UIWebView‘s height constraint to an outlet in your view controller.
  5. Set the view controller as UIWebViewDelegate.
  6. In webViewDidFinishLoad set the height constraint’s constant to the height of the contentSize of the scroll view inside the web view.
  7. Start Key-Value Observing on the contentSize to change the height, when height of the web view has to change because segments of the webpage change their size without reloading the page (like accordeons, or menus). Don’t forget to stop observing when the view controller gets deallocated.

Here is the code that you need:

import UIKit

var MyObservationContext = 0

class ViewController: UIViewController {

    @IBOutlet weak var webview: UIWebView!
    @IBOutlet weak var webviewHeightConstraint: NSLayoutConstraint!
    
    var observing = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        webview.scrollView.isScrollEnabled = false
        webview.delegate = self
        if let url = URL(string: "https://www.google.de/intl/de/policies/terms/regional.html") {
            webview.loadRequest(URLRequest(url: url))
        }
    }
    
    deinit {
        stopObservingHeight()
    }
    
    func startObservingHeight() {
        let options = NSKeyValueObservingOptions([.new])
        webview.scrollView.addObserver(self, forKeyPath: "contentSize", options: options, context: &MyObservationContext)
        observing = true;
    }
    
    func stopObservingHeight() {
        webview.scrollView.removeObserver(self, forKeyPath: "contentSize", context: &MyObservationContext)
        observing = false
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard let keyPath = keyPath else {
            super.observeValue(forKeyPath: nil, of: object, change: change, context: context)
            return
        }
        switch keyPath {
        case "contentSize":
            if context == &MyObservationContext {
                webviewHeightConstraint.constant = webview.scrollView.contentSize.height
            }
        default:
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }
}

extension ViewController: UIWebViewDelegate {
    func webViewDidFinishLoad(_ webView: UIWebView) {
        webviewHeightConstraint.constant = webview.scrollView.contentSize.height
        if (!observing) {
            startObservingHeight()
        }
    }
}

In case you have difficulties implementing this, i put a working example on github

EDIT

GitHub user kenji21 implemented this cool UIWebView subclass that get’s the job done with less code:

class ContentFittingWebView: UIWebView {
    
    var contentSizeObservationToken: NSKeyValueObservation?
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        startObservingHeight()
    }

    override var intrinsicContentSize: CGSize {
        return scrollView.contentSize
    }
    
    func startObservingHeight() {
        contentSizeObservationToken?.invalidate()
        contentSizeObservationToken = scrollView.observe(\UIScrollView.contentSize, options: [.new], changeHandler: { (scrollView, change) in
            self.invalidateIntrinsicContentSize()
        })
    }
}

Dismiss keyboard in HTML form displayed in UIWebView inside modal view controller

Posted: | Author: | Filed under: HTML, iOS | Tags: , | 3 Comments »

Recently I encountered a problem with the keyboard while displaying a HTML form inside a UIWebView: The HTML form was a simple form to ask the login credentials from a user. Nothing more than 2 text input fields and a submit button. When the user tapped on an input field the iOS Keyboard came up, as it should. But then, when the user tapped the submit button, the keyboard stayed in placed and was not dismissed. The normal behaviour would be that the keyboard would be dismissed as soon as the textfield loses it’s focus. But somehow this was not happening.

I found several solutions on how to dismiss the keyboard programatically by either using javascript or calling resignFirstResponder on the UIWebView when the form was being submitted, but the keyboard would still not go away.

Then I found the solution to that problem. I turned out that the lingering keyboard was a UI design choice by Apple. The keyboard dismissal is disabled when you present your UIWebView using a modal ViewController with presentation style UIModalPresentationFormSheet!

The solution is quite simple: You have to override your ViewController’s disablesAutomaticKeyboardDismissal method:

- (BOOL)disablesAutomaticKeyboardDismissal {
    return NO;
}

After that the keyboard will be dismissed as soon as the input fields lose focus (or the form is being submitted).

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.