WKWebView: An Extensive Guide To Web Views

Written by Reinder de Vries on September 11 2018 in App Development

WKWebView: An Extensive Guide To Web Views

You use WKWebView to display interactive web content in your app. Ideal for displaying HTML markup, styled text content, or complete web pages. It’s like having a small web browser right in your app!

In this article you’ll learn:

  • How to use WKWebView with Swift
  • How to respond to events and user interaction with WKUIDelegate
  • Why WKWebView is useful, and in what scenarios
  • Some quick tips, like getting the web page content size

Ready? Let’s go.

  1. Loading A Web Page In WKWebView
  2. Responding To WKWebView Events With WKNavigationDelegate
  3. Navigation, Injecting JavaScript And Content Size
  4. Use Cases For WKWebView
  5. Further Reading

The WKWebView component is a replacement for UIWebView. In iOS 12, UIWebView will be removed from the iOS SDKs and replaced with WKWebView. If your app uses UIWebView, you’ll probably have to upgrade it to use WKWebView. Don’t worry, UIWebView and WKWebView are very similar in nature!

Loading A Web Page In WKWebView

The WKWebView class can be used to display interactive web content in your iOS app, much like an in-app browser. It’s part of the WebKit framework and WKWebView uses the same browser engine as Safari on iOS and Mac.

Adding a web view to your app is as simple as adding a UIView or UIButton to your view controller in Interface Builder. Here’s how:

  1. Open the XIB or Storyboard you want to add the web view to in Interface Builder
  2. Find the web view or WKWebView in the Object Library at the bottom-left of Interface Builder
  3. Drag-and-drop a WKWebView object from the Object Library to your view controller’s canvas, and adjust its size and position

It’s recommended to create an outlet for the web view in your view controller class, such as:

@IBOutlet weak var webView:WKWebView?

Don’t forget to connect the outlet in Interface Builder! And you’ll need to import the WebKit framework too, at the top of your view controller:

import WebKit

Awesome! Let’s load some interactive web content in that WKWebView object.

First, we’ll create an instance of URLRequest with the information about the URL we want to load. Like this:

let request = URLRequest(url: URL(string: "https://learnappmaking.com")!)

Here’s what happens:

  • We’re creating an instance of URL by providing it a URL string https://learnappmaking.com. The initializer URL(string:) is failable, so it will return nil if the provided string is an invalid URL.
  • We’re 100% certain we’ve provided a valid URL, so we’re using force unwrapping to unwrap the optional.
  • The URL object is then provided to the URLRequest initializer, which then assigns a URLRequest object to the request constant.

Next, we’ll use this request to load the URL in the webview. Like this:

webView?.load(request)

Assuming you’re using a WKWebView in your view controller, you would add the above code to the viewDidLoad() function. Like this:

override func viewDidLoad()
{
    super.viewDidLoad()

    let request = URLRequest(url: URL(string: "https://learnappmaking.com")!)

    webView?.load(request)
}

When you run your app, you’ll see that the web page shows up! And it’s completely interactive, so you can navigate the web page like an ordinary browser.

WKWebView

Learn how to build iOS apps

Get started with iOS 13 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 13 apps with Swift 5 and Xcode 11.

Responding To WKWebView Events With WKNavigationDelegate

The most simple form of a web view is really bare-bones! You can, however, interact with the web view using a number of properties, objects and delegate protocols.

A WKWebView web view has two main delegate protocols and properties:

  • navigationDelegate of type WKNavigationDelegate, which responds to navigation events
  • uiDelegate of type WKUIDelegate, which responds to user interaction events

Of these two delegate protocols, the WKNavigationDelegate is probably used most frequently. So, let’s hook into some page navigation events! We’ll do so with these delegate functions:

  • webView(_:didStartProvisionalNavigation:)
  • webView(_:didCommit:)
  • webView(_:didFinish:)
  • webView(_:didFail:withError:)
  • webView(_:didFailProvisionalNavigation:withError:)

First, let’s adopt the protocol and set the webView delegate. Here’s how:

  1. Add the WKNavigationDelegate protocol to your view controller’s class declaration, like: class WebViewController: UIViewController, WKNavigationDelegate
  2. Set the navigationDelegate property of webView to self, before loading the web request, like: webView?.navigationDelegate = self

Next, implement the five delegate functions described earlier. Set their function bodies to print(#function), so we can see the order in which the functions are called.

Like this:

func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!)
{
    print(#function)
}

Repeat this for the other four delegate functions.

Finally, run the app! In the Console you see the order in which the delegate functions are called:

  1. webView(_:didStartProvisionalNavigation:)
  2. webView(_:didCommit:)
  3. webView(_:didFinish:)

The first function webView(_:didStartProvisionalNavigation:) is called when the web view starts navigating to a page. At this point the URL request has just been sent to the webserver and no response has come back yet.

The second webView(_:didCommit:) function is called when the web view starts receiving data from the web server. At this point it will attempt to load the web page HTML, as data is coming in.

The third function webView(_:didFinish:) is called when the navigation is finished and all data has come in. This event usually coincides with the DOMContentLoaded JavaScript event, but keep in mind that webView(_:didFinish:) can also be called before the HTML DOM is ready.

So, when are the error functions called? Let’s find out!

  • First, change the web page URL of the web view to http://example.com
  • Then, add print(error) to the two error functions. so we can see the error message
  • Finally, run the app again

You should now see an error message in the Console:

The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.

This happens because you’re only allowed to load HTTPS URLs on iOS by default. You can’t load an HTTP URL, like we did, without changing the so-called App Transport Security settings.

So, what do you use all these delegate functions for?

  • You can show a network activity spinner when a page navigation starts
  • And stop the network activity indicator when navigation stops
  • You can show an alert controller dialog when errors occur

And that’s not everything, you can for example use the webView(_:decidePolicyFor:decisionHandler:) function to decide if a page navigation is allowed. And you can respond to redirects, HTTP Authentication challenges, and use crash recovery.

Navigation, Injecting JavaScript And Content Size

A web page and an app are two very distinct environments. Technically, a web page doesn’t “know” it’s running inside a WKWebView. And vice-versa, you can create full-fledged web apps that run inside Safari on iOS.

So, how do you interact with a web page in a WKWebView? You’ve got several options.

Simple navigation and interactions happen directly with functions on the webview instance, such as:

  • load(_:mimeType:characterEncodingName:baseURL:) and loadHTMLString(_:baseURL:) to load web pages, and HTML directly
  • estimatedProgress, isLoading and hasOnlySecureContent to get the state of the web page
  • reload() and stopLoading() to respectively reload and stop loading the page
  • goBack(), goForward() and go(to:) for history-based navigation, like a normal web browser
  • canGoBack, canGoForward and backForwardList to get information about navigation history

And then of course, there’s JavaScript. JavaScript is enabled in a WKWebView by default, and you can use it to do a thousand-and-one things. Unfortunately, JavaScript-based interaction between the web view and your app is… medieval.

You have one function at your disposal: evaluateJavaScript(_:completionHandler:). When you call this function on a WKWebView instance, a string of JavaScript is sent to the web page and evaluated. When evaluation is done, its result is returned with the completionHandler closure.

Let’s execute some JavaScript. Change the webView(_:didFinish:) function into the following:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
{
    webView.evaluateJavaScript("navigator.userAgent", completionHandler: { result, error in
        if let userAgent = result as? String {
            print(userAgent)
        }
    })
}

Here’s what happens when the web view finishes navigation:

  • We send a JavaScript string to the web view. The navigator.userAgent is a browser property that contains the User Agent String, which is a bit of text that identifies the type and make of the browser that’s loading the web page.
  • When the JavaScript has been evaluated, the result is provided to a completion handler. We’ve provided a simple closure that prints out the userAgent constant if it’s a value of type String.

WKWebView

You can evaluate almost any kind of JavaScript code. That means you can inspect elements, check the values of properties, and assign event handlers.

The downside of this approach is that it’s only one way: you can inject JavaScript in the web page, but you can’t respond to events happening in the web view from within the web view itself.

You can use an alternative approach to work with JavaScript and WKWebView, and that’s by using a WKUserScript. Here’s how:

let config = WKWebViewConfiguration()
let js = "document.addEventListener('click', function(){ window.webkit.messageHandlers.clickListener.postMessage('My hovercraft is full of eels!'); })"
let script = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: false)

config.userContentController.addUserScript(script)
config.userContentController.add(self, name: "clickListener")

webView = WKWebView(frame: view.bounds, configuration: config)
view.addSubview(webView!)

Next, make sure to adopt the WKScriptMessageHandler delegate protocol in your view controller’s class declaration. And add the following function to your view controller:

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
{
    print(message.body)
}

What happens here?

The red thread here is that we’re creating an instance of WKWebView manually, and assign it a WKWebViewConfiguration object. This configuration object gets a WKUserScript object, which contains JavaScript code.

The JavaScript code attaches an event listener to the web page. This will post a message to the clickListener message handler any time a click event happens on the web page.

The user script also attaches to that same clickListener handler. So, when the message “My hovercraft is full of eels!” gets posted to the message handler, the delegate function userContentController(_:didReceive:) gets called on your view controller. This effectively sends a message from the web page to your iOS app, and that’s something we couldn’t do before!

If you use the above code sample with the code you’ve written previously in this tutorial, make sure to disable the outlet and remove the WKWebView instance from your view controller in Interface Builder first.

Use Cases For WKWebView

The usefulness of WKWebView goes beyond displaying simple web pages in your app. After all, most of what you can do on a web page you can code natively in your iOS app – so why use web views at all?

One particularly useful scenario is when you want to create a layout or user interface with HTML. Imagine you’re making a recipe app and you want to show detailed information about the recipe, in a detail view controller.

You could create the entire user interface natively, with views, buttons and labels. You could also create that same user interface with HTML and CSS. You could even store the recipe HTML pages on a webserver, and update them on-the-fly.

The WKWebView function you need at this point is loadHTMLString(_:baseURL:). With this function you can load HTML directly in the web view.

Like this:

webView.loadHTMLString("<strong>So long and thanks for all the fish!</strong>", baseURL: nil)

This will show a bold string of text in the web view. And of course, you can load any kind of HTML and CSS in the web view.

The baseURL parameter allows you to set a base URL for the web page, like the HTML <base> tag. Any URL on the web page will load relatively from your base URL.

Keep in mind that any HTML page needs <html> and <body> tags, although a web view will also function OK without them.

Here’s a useful snippet that sets an initial viewport with, and uses iOS’s default San Francisco font:

<html>
    <head>
        <style>body { font-family: -apple-system, Helvetica; sans-serif; }</style>
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
        \(html)
    </body>
</html>

When you assign the above snippet to your web view, make sure that the variable html contains your page’s HTML string.

WKWebView

And here’s something else… Imagine you’re building your recipe app and you want to display a web view below some native views, such as an image view and a few labels. You add these views, including the web view, in a scroll view, so the user can scroll the entire stack of views from top to bottom.

You now have a problem, because both the scroll view and the web view can scroll!

The solution here is as follows:

  • Pin the web view to the leading, trailing, top and bottom edges with Auto Layout constraints
  • Give the web view a dynamic height constraint, i.e. connect the constraint to the outlet so you can set it’s constant property
  • Get the inner content size of the web page, and dynamically set the height of the web view to match the height of the web page (with the constraint)
  • Optionally, disable scrolling on the web view

As a result, the web view resizes itself to match its inner content height. This will fit vertically in the scroll view, which you can now scroll from top to bottom without scrolling the web view itself.

First, make sure you’ve set up the constraints of your web view and added an outlet of type NSLayoutConstraint for the heightConstraint of the web view itself.

Then, add the following code to your view controller:

webView.evaluateJavaScript("document.readyState", completionHandler: { result, error in

    if result == nil || error != nil {
        return
    }

    webView.evaluateJavaScript("document.body.offsetHeight", completionHandler: { result, error in
        if let height = result as? CGFloat {
            self.heightConstraint?.constant = height
        }
    })
})

Sample code adapted from IvanMih‘s.

The above code evaluates two bits of JavaScript. It’ll first read the readyState of the web page, and then it’ll read the offsetHeight of the document body. This matches the height of the web page, which we subsequently use to set the heightConstraint on the web view.

The above code works fine with iOS 12 and Swift 5. Unfortunately, web pages typically have a lot of moving pieces, so your mileage may vary. What’s important is to evaluate the height of the web page as late as possible, but not later. You want to get the content height as soon as the page has finished loading completely.

Learn how to build iOS apps

Get started with iOS 13 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 13 apps with Swift 5 and Xcode 11.

Further Reading

And that concludes our exploration of WKWebView. The web view is quite a powerful component, albeit tricky to work with too.

We’ve looked at loading a simple web page in WKWebView and responding to navigation events. You’ve inserted JavaScript into the page, with two methods, and used simple commands to navigate and inspect the web view. And we’ve looked at different scenarios in which a web view might come in handy.

Want to learn more? Check out these resources:

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.