Using JavaScript with WKWebView in iOS 8

With iOS 8 Apple has added a ton of user-facing goodness. The Health app, Apple Pay, and expanded TouchID capabilities—just a few things everyday users will be happy about. On the SDK side they’ve added a lot of cool things as well, but one I’m excited about is the addition of WKWebView. This is very similar to the related–but less powerful–UIWebView available since iOS 2. UIWebView offers simple methods for loading a remote url, navigating forwards and back, and even running basic JavaScript. In contrast WKWebView offers a full-blown configuration (via WKWebViewConfiguration), navigation (via WKNavigationDelegate), estimated loading progress, and evaluating JavaScript.

In this example I’m using two components, evaluateJavaScript on the web view and addScriptMessageHandler on the controller to demonstrate passing information between the JavaScript runtime and the native application. This bidirectional communication enables all sorts of interesting hybrid application possibilities.

Overview

High-level Communications

At a high level passing information from native code to the JavaScript runtime is done by calling the evaluateJavaScript method on a WKWebView object. You can pass a block to capture errors but I’m not exploring that here. Passing information from JavaScript land to the iOS application uses the overly verbose Objective-C-style window.webkit.messageHandlers.NAME.postMessage function where NAME is whatever you call the script handler.

Creating Objects

Code Diagram

There are three main objects that need to be setup for this to work.

  • WKWebView instance (called webView)
  • WKWebViewConfiguration instance (called configuration)
  • WKUserContentController instance (called controller)

The constructor for WKWebView takes a configuration parameter. This allows an instance of WKWebViewConfiguration to be passed and additional settings configured. The important property is userContentController, an instance of WKUserContentController. This controller has a method called addScriptMessageHandler which is how messages from JavaScript land are sent to the native application. This is a big chunk of boilerplate that needs to get setup before the WKWebView can be loaded. Thankfully it’s not all bad.

Oh right, the ViewController needs to match the protocol defined by WKScriptMessageHandler. This means implementing the userContentController delegate method. Onwards to the code examples.

Implementation

Start by importing WebKit and declaring that ViewController implements the WKScriptMessageHandler protocol. This is done in the header file (ViewController.h).

#import <WebKit/WebKit.h>
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <WKScriptMessageHandler>


@end

Moving to ViewController.m, start with the URL constant and @interface declaration. I’ve setup a JSBin which helps with the example.

#define k_JSBIN_URL @"http://jsbin.com/meniw"

@interface ViewController ()
@property (strong, nonatomic) WKWebView *webView;

@end

The guts of the next part relate to the overly-verbose object instantiations described above.

Creating a WKWebViewConfiguration object so a controller can be added to it.

    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc]
                                             init];

Creating the WKUserContentController.

WKUserContentController *controller = [[WKUserContentController alloc]
                                       init];

Adding a script handler to the controller and setting the userContentController property on the configuration.

[controller addScriptMessageHandler:self name:@"observe"];
configuration.userContentController = controller;

Create an NSURL object and instantiate the WebView.

NSURL *jsbin = [NSURL URLWithString:k_JSBIN_URL];

_webView = [[WKWebView alloc] initWithFrame:self.view.frame
                              configuration:configuration];

Load up webView with the URL and add it to the view.

[_webView loadRequest:[NSURLRequest requestWithURL:jsbin]];
[self.view addSubview:_webView];

The last piece is setting up the didReceiveScriptMessage method with some handling logic for processing received messages. Here I demonstrate dynamically pulling information from the device using the message passed from JavaScript-land.

- (void)userContentController:(WKUserContentController *)userContentController
      didReceiveScriptMessage:(WKScriptMessage *)message {

    // Log out the message received
    NSLog(@"Received event %@", message.body);

    // Then pull something from the device using the message body
    NSString *version = [[UIDevice currentDevice] valueForKey:message.body];

    // Execute some JavaScript using the result
    NSString *exec_template = @"set_headline(\"received: %@\");";
    NSString *exec = [NSString stringWithFormat:exec_template, version];
    [_webView evaluateJavaScript:exec completionHandler:nil];
}

Of course, none of this would work well without some JavaScript running on the other end.

Start by setting up a bare-bones HTML page.

<h2 id="headline">loading...</h2>
<select id="selector">
    <option value="systemVersion" selected>iOS Version</option>
    <option value="systemName">System Name</option>
    <option value="name">Device Name</option>
    <option value="model">Device Model</option>
    <option value="userInterfaceIdiom">User Interface</option>
    <option value="identifierForVendor">Vendor ID</option>
</select>

Then sprinkle in some JavaScript with functions to send a message to the native side and handle a receiving call.

var headline = $("#headline");
var selection = $("#selector");

function set_headline (text) {
    headline.text(text);
}

function call_native () {
    var prop = selection.val();
    set_headline("asked for " + prop + "...");
    window.webkit.messageHandlers.observe.postMessage(prop);
}

setTimeout(call_native, 1000);

selection.on("change", call_native);

Here’s a quick demo video of everything working.

Full source for both the iOS application and HTML components is on GitHub under joshkehn/JSMessageExample.


Future Applications

With these new APIs available in iOS 8 it will be interesting to see the direction existing projects—like Cordova and Appcelerator Titanium—take and what new frameworks and tooling is built to better support these hybrid applications. Is Apple now encouraging this kind of hybrid application? I’m sure they are. Fully native applications offered speed and performance in the early days of mobile, but with new optimizations to JavaScript engines and better CSS/HTML features that gap is rapidly closing. I’m excited to see what comes next.

Comment on reddit’s /r/programming/.

written October 29th, 2014

October 2014

Can’t find what you’re looking for? Try hitting the home page or viewing all archives.