July 15, 2014

Swift to JavaScript and JavaScript to Swift. A round trip.

Posted in Uncategorized tagged , , , , , at 9:12 pm by tetontech

Edit: Since originally writing this post I have Created the SwiftlyHybrid and AndyHybrid libraries and put them on gitHub. If you are interested in the topic discussed here I would suggest also reading the SwiftlyHybrid and AndyHybrid follow up posting.

In a previous post I showed how to make a call from JavaScript to Swift. Take a look there for a more in-depth discussion of the code to accomplish that data transfer. This example expands on what I did in that post by showing how to make a call from JavaScript to Swift, do a calculation, and then pass the results back to JavaScript. The source code is very similar. It uses one method, evaluateJavaScript(), of the WKWebView class that wasn’t available in Apple’s previous Swift beta.

In the view controller’s userContentController method we’ll need to have access to the WKWebView created in the viewDidLoad method. This requires the addition of a WKWebView attribute to the ViewController class. After trying a couple of experimental ideas I remembered the correct, Swifty way to do this. I couldn’t create the WebView on the same line as the attribute was declared since the initializer for the WKWebView isn’t as simple as adding some integer attribute (var someInt = 3). Instead the initializer needs a frame and a WKWebViewConfiguration. Because of this the WKWebView must be initialized in the viewDidLoad method (This may change if Apple adds a WKWebView to Interface Builder).

The Swift way to declare an attribute and initialize it in something other than an init method is to make the attribute optional using the ? operator.

class ViewController: UIViewController, WKScriptMessageHandler {
    
    var theWebView: WKWebView?
}

In the viewDidLoad method the WKWebView can now be created using the WKWebView function. Once theWebView has been set, the ! operator is needed to declare that we know theWebView attribute is not nil.

(Warning: the functionallity of WKWebView has changed since this code was written. You must move all resources (html, JavaScript, Css, images, and other files) into the temp directory or WKWebView will not load the resources. This may change in the future as WebKit changes or it might not.)

    override func viewDidLoad() {
        super.viewDidLoad()
        var path = NSBundle.mainBundle().pathForResource("index", 
                               ofType: "html")
        var url = NSURL(fileURLWithPath: path)
        var request = NSURLRequest(URL: url)

        var theConfiguration = WKWebViewConfiguration()
        theConfiguration.userContentController.addScriptMessageHandler(self, 
                                   name: "interOp")

        theWebView = WKWebView(frame: self.view.frame, 
                             configuration: theConfiguration)
        theWebView!.loadRequest(request)
        self.view.addSubview(theWebView)

    }

As in the previous example, the userContentController() method must be implemented to handle the messages captured by the script message handler that was added to theConfiguration. In this example the message’s body, the data sent from the JavaScript is interpreted as an NSDictionary since the JavaScript sends an associative array.

    func userContentController(userContentController: 
       WKUserContentController!,
       didReceiveScriptMessage message: WKScriptMessage!) {
        
        let sentData = message.body as NSDictionary
        let aCount:Int = Int(sentData["count"] as NSNumber)
        
        theWebView!.evaluateJavaScript("storeAndShow( \(aCount + 1) )", 
                                        completionHandler: nil)
    }

The other change to the previous example is the addition of the call to theWebView’s evaluateJavaScript method. It has two parameters; a string that is the Javascript to be executed, and a function or closure that will be notified once the JavaScript completes. In this example I don’t need to know when the JavaScript completes so I passed nil as the second parameter.

The javascript for this example is strait forward; an onclick listener function to send a message to the Swift message handler, and the storeAndShow function that the Swift code will call to send a message back to the JavaScript.

var count = 0

function sendCount(){
    var message = {"count":count}
    window.webkit.messageHandlers.interOp.postMessage(message)
}

function storeAndShow(updatedCount){
    count = updatedCount
    document.querySelector("#resultDisplay").innerHTML = count
}

For my own code I would send a Dictionary back to the JavaScript where it would be used as an associative array. I left that out of this example since I didn’t think it was necessary to show.

Overall, the interaction between Swift and Javascript using the WKWebView is more strait forward than using the old UIWebView. It is nice to have a JavaScript way to directly call to Swift. I also like the selection of JSON as the data transfer format for the JS to Swift communication. It reflects the decision I made to adopt that format for QCHybrid years ago. It would be better if the values in the dictionary knew their type so casting using the as operator wasn’t needed but I can live with it the way it is.

Advertisements

6 Comments »

  1. Jim Soho said,

    You don’t need the extra variable (theWebView). It’s even simpler:

    message.webView.evaluateJavaScript(“storeAndShow( \(aCount + 1) )”, completionHandler: nil)

    as this is true: message.webView == self.theWebView

    • tetontech said,

      It is true. The message does contain the WKWebView from which the message is sent. In apps that have multiple WKWebViews this is very useful. In this example, and in most hybrid application development, there is only one WKWebView in the app and since we had to create and add the WKWebView programmatically anyway it seemed just as easy to create a property of the ViewController class and show people how to do that in Swift using an optional.

      That being said, you are right. No property is required. You could just use the WKWebView’s webView property.

  2. Chris2014 said,

    Thank for your tutorial. the evaluateJavaScript work fine with number but it will not accept string as parameter. Is there a way to call javascript function and pass a couple of parameters

    Here is my swift code

    func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage){
    let sentData = message.body as NSDictionary
    let aCount:Int = Int(sentData[“getData”] as NSNumber)
    println(aCount)
    var sentString = “welcome”
    theWebView!.evaluateJavaScript(“showString(\(sentString))”,
    completionHandler: nil)
    }

    Here is my javascript function. Swift code supposes to call and pass parameters. HTML and Script are in one file

    function sendCount(){
    var message = {“getData”:count}
    window.webkit.messageHandlers.interOp.postMessage(message)
    }

    function showString(passString){
    document.querySelector(“#resultDisplay”).innerHTML = passString
    }

    • Chris2014 said,

      OK I already figured it out.

  3. Manasa said,

    Well written tutorial , very detailed explanation .Could you let me know if there is a way to do this without using WKWebView ? i’m using UIWebView .


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: