June 13, 2014

Swift, WKWebView, and Calling from Swift to Javascript

Posted in Uncategorized tagged , , , , at 7:34 pm by tetontech

Update: If you are interested in this topic you may find this new post helpful. It describes and updated version of the example code you see here and standardizes it for both Swift and Android.

In a previous post, I showed how to embed Apple’s new WKWebView using Swift to create a ‘full view’, hybrid JavaScript application. An ability that QC Hybrid and PhoneGap, both early hybrid development platforms, had was to allow calls to be made from Javascript down to the native language of the device. In the case of iOS that was, and can still be, Objective-C but now can also be done in Swift. The new WKWebView has new functionality available in both languages that requires much less overhead to accomplish this interoperability than the old iOS UIWebView and Android WebView. The WKWebView currently does have an important limitation I’ll discuss in a moment.

There are two parts needed to achieve iOS’s new interoperability; a ‘listener’ of type WKScriptMessageHandler on the Swift side and a new object that has a postMessage() method on the Javascript side. When the Javascript postMessage() method is passed an array, associative array, object, or other Javascript type, the postMessage() method will JSON what it was passed. It then pushes this JSON string as a message to a queue on the Swift side of the application. At this point the JSON is parsed and the message becomes the matching Swift types, Array, Dictionary, String, Int, Double, etc. Custom Javascript objects become Dictionaries. Now your code in the WKScriptMessageHandler’s didRecieveMessage() method has Swift objects without needing to use JSON libraries in either the Javascript or Swift portions of your code. This is great. Unfortunately, at the time of this posting, there is no matching functionality in the beta release to easily push Swift objects back up to Javascript. Hopefully this will come soon. If not, there will need to be some ‘hack-like’ work-arounds similar to what QC Hybrid and PhoneGap had to do. This would be unfortunate.

The source code for this posting’s example is a modification of the previous post’s source. Rather than repeat the discussion of those portions of the example I’d suggest you review the previous post about how to do the embedding. The example below is the contents of a ViewController.swift file. It includes the changes needed to setup JS -> Swift interoperability (JS->Objective-C would use the same classes and methods) for an iOS single view application. The changes can be seen in lines 4, 14-18, and 29 – 31.

Line 4 declares the ViewController class as conforming to the WKScriptMessageHandler protocol. Notice that unlike Objective-C, Swift protocol declarations look just like object inheritance calls (in this case ViewController inherits from UIViewController). Swift only allows single inheritance, like Java. When doing both inheritance and protocol declarations the inheritance declaration must come first. If no inheritance is being done then a protocol declaration can be the first item after the : operator.

(Note: This code example has been updated for Swift 2.0 on my gitHub repository.)

1 import UIKit
2 import WebKit
4 class ViewController: UIViewController, WKScriptMessageHandler {
6    override func viewDidLoad() {
7        super.viewDidLoad()
9        //prepare
10        var path = NSBundle.mainBundle().pathForResource("index", 
                                        ofType: ".html")
11        var url = NSURL(fileURLWithPath:path)
12        var request = NSURLRequest(URL:url)
14        //changes from last post
15        var theConfiguration = WKWebViewConfiguration()
16        theConfiguration.userContentController.addScriptMessageHandler(self, 
                                        name: "interOp")
17        var theWebView = WKWebView(frame:self.view.frame, 
                                        configuration: theConfiguration)
18        //end of changes
20        theWebView.loadRequest(request)
21        self.view.addSubview(theWebView)
22    }
24    override func didReceiveMemoryWarning() {
25        super.didReceiveMemoryWarning()
26        // Dispose of any resources that can be recreated.
27    }
29    func userContentController(userContentController: WKUserContentController!, didReceiveScriptMessage message: WKScriptMessage!){
30        println("got message: \(message.body)")
31    }
33 }

Line 15 introduces a new class, WKWebViewConfiguration. This class manages various types of configurations for any and all WKWebViews used in your applications. One of the WKWebViewConfiguration’s properties is a content controller named userContentController. This is the object we need to access in order to setup the ViewController class as the ‘listener’ for messages from Javascript.

Line 16 shows how to assign the ViewController to be the Javascript message handler for any messages generated as part of the named message queue ‘interOp’. You can select any name you want instead of interOp. It just sounded good to me.

Line 17 shows how to initialize the WKWebView with both a frame and the WKWebViewConfiguration instance. This tells the WKWebView to create an opportunity to use the Javascript postMessage() method.

We’re almost done. The only other change needed is to create the ‘listener’ method, userContentController.didRecieveScriptMessage(). One of the parameters to this method will be the WKScriptMessage generated as part of the interoperation between your JavaScript and Swift. The message has a property named body that is the Swift version of the JSON data passed from JavaScript. Line 30 shows this example printing the Swift objects that the example sends from the Javascript side.

I created a simple Javascript function in the project’s main.js file that you can see below. Notice the new window.webkit.messageHandlers object. It has an attribute ‘interOp’ that matches the name we used on line 16 of the Swift code. The JavaScript interOp object is the ViewController since it conforms to the WKScriptMessageHandler protocol and was assigned on line 16 with the interOp name.

function sendMessage(){
    var aMessage = {'command':'hello', data:[5,6,7,8,9]}

That’s it. Now we can send any object, array, or primitive as a message to Swift. Hopefully Google will not re-invent the wheel when they add this same type of interoperability to Android. They should make their Javascript side look exactly this same way. There is some advantage to copying.

For an example of how to call from JavaScript to Swift and then back to JavaScript see this other post.


%d bloggers like this: