Using WebKit to call WKWebView Javascript from Swift and Swift from Javascript

Many mobile applications incorporate remote web pages, either as passive (static) content — or as in this case as integral parts of the UI. Using the WebKit/WKWebView techniques presented here, your native apps can be better integrated with web content and provide a superior experience to end-users.

Two-way Integration between Swift and JavaScript

In this article we’ll build a full working example of a hybrid native/web application that uses two-way function calls between a native iOS app (written in Swift) and a mobile web page (written in HTML/JavaScript).

By leveraging these two features allows us to build a highly robust, hybrid application where the native and web components cooperate as equal partners in delivering a valuable customer solution.

Solution Overview

The finished solution consists of two components:

  1. A native iOS application, developed in Swift
  2. A static HTML/JavaScript web page hosted on a remote web server (hosted in Microsoft Azure in this case).

The finished learning app implements three main features:

#1 — Loading the web page from the remote. If you’ve used a WKWebView, you know all about this feature. As a UIViewController is loaded, a web page URL is set to the WKWebView, which uses an HTTP GET method to fetch the HTML content.

#2 — Manipulate WebView appearance from Swift. Next we gently wade into the interop waters by sending a command to the WKWebView content page to change the page background color according to a user selection in a native Segment control.

#3 — Callback to Swift from HTML/JavaScript. Finally, we make the solution more complex and interesting by exposing a geolocation function in the native iOS application to the web page. When the user enters an address presses a button on the web view page, the following will be done:

  1. The web page (using JavaScript) calls a Swift function, passing in the user-entered address as a JSON object.
  2. The Swift native app makes an asynchronous call to Apple using CLLocation, to determine the latitude & longitude of the user-entered address.
  3. When the latitude/longitude are returned from Apple, the Swift native app calls a JavaScript function in the web page to update the web page with the latitude/longitude for the entered address.

Solution Demo

Before walking through the code, let’s demo what the completed application looks like (animated GIF).

UI Storyboard Design

The learning application contains a single UIViewController named ViewController. ViewController has only two UI controls in the Storyboard:

  1. A UISegmentedControl which allows the user to change the WebView background color to one of five colors.
  2. A UIView, which is placed in the Storyboard to serve as a container view for the WKWebView control.

Changing Web Page Color

To wade into the hybrid solution water, let’s implement a simple call from Swift to the WKWebView.

ViewController has a member array of colors corresponding to the color choices in the Segment control at the top of the native view.

let colors = [“black”, “red”, “blue”, “green”, “purple”]

When the user taps a new segment in the Segment control, an event handler calls the JavaScript function changeBackgroundColor, passing the string corresponding to the user selection:

@IBAction func colorChoiceChanged(_ sender: UISegmentedControl) {
   webView.evaluateJavaScript
   (“changeBackgroundColor(‘(colors[sender.selectedSegmentIndex])’)”, 
   completionHandler: nil)
}

The Swift code doesn’t really know that the web page has a JavaScript routine named changeBackgroundColor. It’s job is to format a JavaScript fragment that will successfully run in the WebView.

The HTML content in the WKWebView has the matching JavaScript routine, which simply sets the background color of the page to the string passed to it from Swift:

function changeBackgroundColor(colorText) {
    document.body.style.background = colorText;
}

Setting up a Message Handler

The next feature is to send a user-entered address from the HTML page to the native Swift app for geocoding. There are three steps to implement this feature:

  1. Add a message handler to the WKWebView’s WKUserContentController. This establishes a contract that promises that the Swift code can respond to the named message handler when it’s called from the HTML page via JavaScript.
  2. Implement the WKScriptMessageHandler delegate method didReceive message to receive the call from JavaScript.
  3. Call the message handler from the web content JavaScript.

Create a Message Handler (1)

// A
let contentController = WKUserContentController();
contentController.add(self, name: “geocodeAddress”)
// B
let config = WKWebViewConfiguration()
config.userContentController = contentController
// C
webView = WKWebView(frame: webViewContainer.bounds, configuration: config)

A WKUserContentController is created at (A). The contentController holds the registration of the geocodeAddress message handler.

The WKUserContentController is added to a new WKWebViewConfiguration at (B).

Finally (C), as the WKWebView is instantiated, the configured WKWebViewConfiguration created in (B) is passed in to the initializer.

Implement the WKScriptMessageHandler delegate (2)

Now that the geocodeAddress handler is defined to the WKWebView, we need to implement a delegate method, which is triggered when WKWebView event handlers are called.

In this solution, an extension is defined to implement the WKScriptMessageHandler protocol on the ViewController class.

extension ViewController:WKScriptMessageHandler {
   func userContentController(
           _ userContentController:
           WKUserContentController, 
           didReceive message: WKScriptMessage) {
    if message.name == “geocodeAddress”, 
       let dict = message.body as? NSDictionary {
            geocodeAddress(dict: dict)
       }
    }
}

The didReceive handler checks whether the message name is as expected (geocodeAddress), and if so extracts the JSON object from the message body (as an NSDictionary), and calls the ViewController instance method geocodeAddress.

Note that the message handler is stringly typed, so be careful that the string comparison in didReceive properly matches the original message handler registration made with the WKUserContentController.

Calling geocodeAddress from the HTML/JavaScript page (3)

In HTML, the form’s INPUT button calls a JavaScript function called geocodeAddress:

<input type=”submit” value=”Geocode Address” onclick=”geocodeAddress();”>

The body of the JavaScript geocodeAddress function responds by calling the Swift Message Handler of the same name, passing in address details as a JSON object.

function geocodeAddress() {
    try {
        webkit.messageHandlers.geocodeAddress.postMessage(
            {
                street: document.getElementById(“street”).value,
                city: document.getElementById(“city”).value,
                state: document.getElementById(“state”).value,
                country: document.getElementById(“country”).value
            });
        document.querySelector(‘h1’).style.color = “green”;
    } catch(err) {
        document.querySelector(‘h1’).style.color = “red”;
    }
}

Note: In the JavaScript geocodeAddress() function, the H1 style changes are merely here for testing purposes and are not part of the actual solution.

Passing back Latitude/Longitude to the HTML page

So far, the HTML page has accepted an address entry from the user in a series of INPUT fields, and sent it to the native Swift application. Now let’s complete the final requirement — geocoding the address and returning it to the web page UI.

Recall that the Swift message handler calls a Swift function called geocodeAddress(dict:) to do the heavy-lifting of geocoding the address.

func geocodeAddress(dict: NSDictionary) {
    let geocoder = CLGeocoder()

    let street = dict[“street”] as? String ?? “”
    let city = dict[“city”] as? String ?? “”
    let state = dict[“state”] as? String ?? “”
    let country = dict[“country”] as? String ?? “”

    let addressString = “(street), (city), (state), (country)”
    geocoder.geocodeAddressString(
        addressString, 
        completionHandler: geocodeComplete)
}

This part of the solution is straightforward CoreLocation. After the geocodeAddressString asynchronous function sends the address to Apple, the response is provided to the Swift method geocodeComplete:

func geocodeComplete(placemarks: [CLPlacemark]?, error: Error?) {
    if let placemarks = placemarks, placemarks.count > 0 {
        let lat = placemarks[0].location?.coordinate.latitude ?? 0.0
        let lon = placemarks[0].location?.coordinate.longitude ?? 0.0
        webView.evaluateJavaScript(
            “setLatLon(‘(lat)’, ‘(lon)’)”, completionHandler: nil)
    }
}

This method checks to make sure at least one placemark was found for the provided address, extracts the latitude and longitude from the first place mark, and then sends them back to the HTML page by calling its setLatLon JavaScript function.

Updating the HTML page

The process of sending the latitude/longitude back to the web page is functionally identical to the previous feature which set the background color.

The setLatLon JavaScript function is implemented as follows:

function setLatLon(lat, lon) {
    document.getElementById(“latitude”).value = lat;
    document.getElementById(“longitude”).value = lon;
}

As with the background color function, setLatLon simply sets the HTML form’s INPUT field values to the passed parameter values.

Summary

The most common use of WKWebView to provide a simple display of web content within the context of a native iOS application — but it can do much more, and in this article we’ve seen how to incorporate web and native components to build enhanced native applications, or even hybrid native/web applications.

Download the Code

The above fragments provide the core functionality for the learning solution. The full Xcode project can be downloaded from Github here.

Leave a Reply

Your email address will not be published. Required fields are marked *