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
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
There are three main objects that need to be setup for this to work.
WKWebView
instance (calledwebView
)WKWebViewConfiguration
instance (calledconfiguration
)WKUserContentController
instance (calledcontroller
)
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
#import
@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.
id="headline">loading...
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.
http://www.joshuakehn.com/2014/10/29/using-javascript-with-wkwebview-in-ios-8.html