WKWebView and JavaScript in iOS 8 - using Swift

Note that the code in this post is outdated

In this article I'll be briefly covering using WKWebView (new in iOS 8 and OS X 10.10) to communicate with content in a web-page using user scripts and script messages. This basically means injecting JavaScript into a web-page and receiving messages from the web-page whenever the web-page call back to your native application. All the example code will be written using Apple new language - Swift and of course JavaScript.

This example builds upon the Getting started with WKWebView using Swift in iOS 8 article which explains how to get started with WKWebView, read that one first.

There are two main concepts you'll need to understand when using the new WKWebView for JavaScript communication. These are user scripts and script messages.

User scripts

In short, user scripts are pieces of JavaScript which are injected into the web-page the WKWebView is loading. A user script is either injected and run before the content (DOM) has loaded or after the DOM is finished loading. A user script can do anything a "regular" JavaScript script can do on the page, including manipulating the DOM and calling any existing JavaScript methods in the page which was loaded. A user script is how your native application talks to JavaScript.

Script messages

This is how any script on the web-page can communicate back to your native app. A script message exposes a method in JavaScript which any can be called by any script in the web-page. You will need to define a handler in you native app which handles incoming messages from the web-site. A script message can originate from a user script or any other script loaded into the web-page handled by the WKWebView.

The demo application

In this small demo application we'll look at how to change the color of a DOM element by calling a JavaScript method from our native application while at the same time listening for asynchronous messages sent from the JavaScript in the html page to the native application. Let's get started!

To communicate between a web-page and WKWebView we need two main elements.

  1. A web-page with some JavaScript for which the WKWebView can load
  2. A WKWebView native application which talks to the web-page

The web-page

The web-page is really simple. There are two files, one «index.html» file which is the web-page and one «main.js» file which «index.html» includes at the end of its body tag.

The content of the JavaScript «main.js» is as follows. (the complete code including the HTML can be found at the bottom of the article)

function callNativeApp () {
    try {
        webkit.messageHandlers.callbackHandler.postMessage("Hello from JavaScript");
    } catch(err) {
        console.log('The native context does not exist yet');
    }
}

setTimeout(function () {
    callNativeApp();
}, 5000);

function redHeader() {
    document.querySelector('h1').style.color = "red";
}

When this script loads it will wait 5 seconds and then call the «callNativeApp» method. The only thing to note here is the "callbackHandler". This is the name of the script message handler we will define later in the native application.

Also, I'm wrapping the callback to "webkit.messageHandlers....." in a try catch block to avoid the script falling over when it's running outside of a native app context.

There is also a tiny function which will change the header in the HTML page to a red color when run, well use this later.

You can load this web-page from a server or simply include it in your app. I'll be loading it from a local server to simulate a more realistic example.

The native application

The first thing you need to do is to point the WKWebView we created in the previous article to the new test URL. You'll need to include you own path here. If you're wondering how to set this up please refer to the Getting started with WKWebView using Swift in iOS 8 article this article build upon.

var url = NSURL(string:"http://localhost/WKJSDemo/")
var req = NSURLRequest(URL:url)
self.webView!.loadRequest(req)

The WKWebView configuration

In «Getting started with WKWebView using Swift in iOS 8» we initialized the WKWebView with no parameters, like so: self.webView = WKWebView()

This is fine for simply loading a web-page. Now however we want to inject some script into the page and this calls for some additional configuration.

The WKWebView constructor has a «configuration» parameter which takes a WKWebViewConfiguration instance. Several things can be set on this, like settings for the WebView for example turning JavaScript on or off and so on. What we need though is the userContentController property of the WKWebViewConfiguration.

The userContentController property expects an instance of WKUserContentController which has a method called addUserScript. We utilize this method to add the user script. Phew! Let's look at a simple example.

The first thing to do is to create a content controller. 

var contentController = WKUserContentController();

Now we need a user script to pass into the addUserScript method of the content controller.

var userScript = WKUserScript(
    source: "redHeader()",
    injectionTime: WKUserScriptInjectionTime.AtDocumentEnd,
    forMainFrameOnly: true
)

contentController.addUserScript(userScript)

Our JavaScript (source) is in this case a call to the «redHeader» function we defined in the JavaScript file. The «injectionTime» parameter tells the user script that it should be run when the body of the HTML page has been loaded. The other way of doing this would be to specify «AtDocumentStart» which would run the script before the body element was loaded. The «forMainFrameOnly» parameter simply says that this script will only be injected for the main frame of the HTML page. The next line adds the userScript to the contentController.

Great, now we need to create a configuration and then add the contentController to it and pass the configuration into the WebView constructor.

var config = WKWebViewConfiguration()
config.userContentController = contentController
        
self.webView = WKWebView(
    frame: self.containerView.bounds,
    configuration: config
)

Now try to run it, you should see a HTML page with a red header.

Note that the WKWebView constructor requires a "frame" parameter. This tells the web view how big it should be and is very common pattern in UIKit classes.

Having the JavaScript call the native app

Now that we know how to call JavaScript methods from the native app we'll want to let the JavaScript methods call back to the native app. As we have discussed, this is done by utilizing «script messages».

Adding the delegate method

To be able to receive events from JavaScript your ViewController needs to conform the «WKScriptMessageHandler» protocol. This means two things. We need to inherit from «WKScriptMessageHandler» and implement the «userContentController» delegate method. Let's start with extending the suprtclass.

class ViewController: UIViewController, WKScriptMessageHandler

Here we update the ViewController to include the «WKScriptMessageHandler». This will give you an error, this is because we haven't yet implemented the «userContentController» delegate method. We'll do this now.

func userContentController(userContentController: WKUserContentController!, didReceiveScriptMessage message: WKScriptMessage!) {
    if(message.name == "callbackHandler") {
        println("JavaScript is sending a message \(message.body)")
    }
}

Notice that we check if the message name is «callbackHandler». Remember back to that line in the JavaScript which read «webkit.messageHandlers.callbackHandler.postMessage..». Here in the native method which receives the script message we need to verify that the message is what we expect it to be. If the message is what we expect we print the body of the message to the console.

Next up, we need to tell the web view to start listening for events from JavaScript. This is done by adding a script handler to the contentController. So, just below the line where we call «addUserScript», we now add:

contentController.addScriptMessageHandler(
    self,
    name: "callbackHandler"
)

The first parameter «self» means that the script message delegate is the ViewController. If you wanted to handle script messages in another class you could pass that class here. The name is the name which will be used in the JavaScript to call the native «userContentController» delegate method.

Try to run it. The header should now be read and after five seconds you should get a message in the Xcode console. Success!

All the code

Web-page

index.html

<!DOCTYPE html>
<html>
    <head>
        <style type="text/css">
            body {
                padding-top: 40px; 
            }
        </style>
        <title>WKWebView Demo</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>WKWebView Test</h1>
        <script type="text/javascript" src="main.js"></script>
    </body>
</html>

main.js (JavaScript)

function callNativeApp () {
    try {
        webkit.messageHandlers.callbackHandler.postMessage("Hello from JavaScript");
    } catch(err) {
        console.log('The native context does not exist yet');
    }
}

setTimeout(function () {
    callNativeApp();
}, 5000);

function redHeader() {
    document.querySelector('h1').style.color = "red";
}

ViewController.swift

import UIKit
import WebKit

class ViewController: UIViewController, WKScriptMessageHandler {
    
    @IBOutlet var containerView : UIView! = nil
    var webView: WKWebView?
                            
    override func loadView() {
        super.loadView()
        
        var contentController = WKUserContentController();
        var userScript = WKUserScript(
            source: "redHeader()",
            injectionTime: WKUserScriptInjectionTime.AtDocumentEnd,
            forMainFrameOnly: true
        )
        contentController.addUserScript(userScript)
        contentController.addScriptMessageHandler(
            self,
            name: "callbackHandler"
        )
        
        var config = WKWebViewConfiguration()
        config.userContentController = contentController
        
        self.webView = WKWebView(
            frame: self.containerView.bounds,
            configuration: config
        )
        self.view = self.webView!
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var url = NSURL(string:"http://localhost/~jornki/tests/WKDemo/")
        var req = NSURLRequest(URL:url)
        self.webView!.loadRequest(req)
    }
    
    func userContentController(userContentController: WKUserContentController!, didReceiveScriptMessage message: WKScriptMessage!) {
        if(message.name == "callbackHandler") {
            println("JavaScript is sending a message \(message.body)")
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

Getting started with WKWebView using Swift in iOS 8

In iOS 8 and Mac OS X Yosemite Apple introduces the new WKWebView and the Swift language among a slew of other amazing stuff. In this article we'll take a quick look at how you can get started writing Swift by implementing a basic browser using the WKWebView class.

For more information about Swift and WKWebView, please visit http://developer.apple.com

Disclaimer: Swift is a brand new language and also new to me, so I'm not stating that the code in this article in any represents the "correct" way of doing things. If you think something is wrong or should have been done differently please do add a comment.

Setting it up

Open xCode 6 and create a Single Page View Application (iOS) project and select a Swift as the project language. This will create a «Main.storyboard» file and a «ViewController.swift» file which are the two files we are concerned with in this article.

Importing WebKit

Open «ViewController.swift» and add an import statement for WebKit using import WebKit, put this line just below the existing UIKit import statement. We need to do this since the WKWebView class is now a part of WebKit itself.

Creating an outlet

Next create an outlet to reference the container view in Interface Builder. Enter the following at the top of the «ViewController» class.

@IBOutlet var containerView : UIView = nil

This piece of code allows the «ViewController» to get a reference to our WKWebView. To finish this connection select the «Main.storyboard» in the assistant editor, so you can see both the storyboard and the «ViewController». To tell the view in interface builder to reference this outlet you can make a connection by dragging from the outlet to the view and letting go.

Creating a variable for the webView

Back in «ViewController.swift», below the containerView outlet make a variable

var webView: WKWebView?

The question mark means that the webView property / variable is a wrapped variable, which again means that this variable might be empty and that it does not have the properties of WKWebView, because it's of type WKWebView? - riiight.

Think about it this way; As the word "wrapped" implies, the webView variable is wrapped inside a "container" which might be empty, but it might also contain something, in our case this is hopefully a WKWebView, but for now it's nothing in it.

We'll get back to what this means in practice when we reference this variable later.

Instantiating the webView

Back in «ViewController.swift» lets create an override for ´loadView´ and write the initialization for the WebView there. This function will look like this:

override func loadView() {
    super.loadView()
    self.webView = WKWebView()
    self.view = self.webView
}

After calling super (which you should always do when overriding methods) we instantiate the webView itself

self.webView = WKWebView()

and then we tell the current view of the viewController (self) that the webView is the view we need to show

self.view = self.webView.

You might be thinking that we could have just instantiated the WKWebView when defining the variable, and we could have, for this simple example. However later on when you actually need to pass parameters to the WKWebView() constructor, this approach will make more sense.

Loading and showing a web-page

Moving on.. lets load a webpage! In the viewDidLoad function, which should be defined in ViewController.swift already, type the following:

override func viewDidLoad() {
    super.viewDidLoad()
    var url = NSURL(string:"http://www.kinderas.com/")
    var req = NSURLRequest(URL:url)
    self.webView!.loadRequest(req)
}

Now run your application! It should load this web-page in the simulator, how's that for meta!

Let's take a look at this code. The first line defines an NSURL using the NSURL(string:) constructor. The next line constructs a NSURLRequest using the url constructed on the previous line. Then we tell the webView to load the request. Notice how self.webView is referenced using the ! at the end. This goes back to the part where we created the webView variable using the ?. Since webView is wrapped it actually refers to the "container" and not the WKWebView instance, which is what we want. Therefore we need to unwrap it and we do this using the !.

Summary

In this article we have briefly touched on using Swift and the brand spanking new WKWebView which is available in iOS 8 and Mac OS X Yosemite. We have seen how a WKWebView can be initialized and rendered to the screen and we have seen how to use Swift when interacting with CocoaTouch.

All the code

The whole ViewController.swift file now looks like this:

import UIKit
import WebKit

class ViewController: UIViewController {
    
    @IBOutlet var containerView : UIView! = nil
    var webView: WKWebView?
                            
    override func loadView() {
        super.loadView()
        
        self.webView = WKWebView()
        self.view = self.webView!
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
       
        var url = NSURL(string:"http://www.kinderas.com/")
        var req = NSURLRequest(URL:url)
        self.webView!.loadRequest(req)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
}

A drawing application in red

The HTML Canvas element was introduced by Apple in 2004. It was originally created to power the Dashboard concept introduced in OS X 10.4. The Canvas element was a part of Apple WebKit from 2004, in 2005 it became a part of Mozilla browsers and then Opera. Today, all browsers worth using sports support for the Canvas element. In this article I'll investigate demo involving a simple line drawing on a Canvas element.

To be a proper red line drawing application today there are several considerations to be taken into account.

  • You must support both touch devices and dusty old mouse enabled devices
  • You must take Hi-res or retina devices into consideration. You wouldn't want blurry red lines now would you.
  • The thing needs to be performant and cool..and red

Take a look at the full CoffeeScript code for the demo. It has a bunch of comments.

Supporting touch and mouse events

if 'ontouchstart' of window
# .. then switch to touch mode
@mode = 'touch'

From line 8 in the code. Here we check if the event «ontouchstart» exists in the window object. If it does we know that this device supports touch events. We then keep this result in the «mode» property for use later on. Easy peasy. Let's add som listeners.

if @mode is 'touch'
canvas.addEventListener 'touchstart', @, false
canvas.addEventListener 'touchmove', @, false
canvas.addEventListener 'touchcancel', @, false
else
canvas.addEventListener 'mousedown', @, false
canvas.addEventListener 'mouseup', @, false

We use the «mode» property and add touch and mouse listeners. Note that we do not add any listeners for when the mouse is moving, this we do in the mouse up and down events. See the section at the end of the article as an explanation as to why.

So, what the hell does it mean to add a listener to "@"? In CoffeeScrip the "@" is the same as "this" in JavaScript. By adding event listeners to "this" we can implement the half-magical «handleEvent» method. This method will not only handle all events on "this", but it will also maintain scope, removing the need for «Function.bind». If you didn't know that, you mind has just been blown - right?! Moving on.

Supporting retina quality graphics

This is a tiny bit trickier, but not much. What we need is to know the scale factor of the display of the device. We can then use this number to calculate the size of the canvas itself and how lines are drawn.

Let's start with finding the scale factor.

@scaleFactor = window.devicePixelRatio || 1;

If there is a property on window called «devicePixelRatio» we use that value, if not we default to 1, which basically means that it's a "normal" display or a piece of shit old browser. The «devicePixelRatio» will output a multiplier, like 2 for the iPad Mini Retina. This number gives us the value on which to multiply.

Scaling the canvas element correctly

Calculating the size of the actual canvas element looks something like this (if you want the canvas to span the entire browser window).

canvas.width = window.innerWidth * @scaleFactor
canvas.height = window.innerHeight * @scaleFactor

On a "normal" display the width of the canvas would be equal to the inner width of the window. On an iPad Mini Retina it would be «innerWidth x 2», because the «scaleFactor» of the iPad Mini Retina is 2.

It's important to note that the canvas element is now twice or more the size of what you intend to display it at. You fix that by setting the style width and height to the intended display size, which in the case of an iPad Mini Retina will be half the size of the canvas backing store (the actual drawing size).

Scaling the drawing context

@ctx = canvas.getContext '2d'
@ctx.strokeStyle = 'rgba(255,0,0,1)'
@ctx.lineWidth = 5 * @scaleFactor

The «ctx» property holds the 2d drawing context of the canvas. On the second line we tell the context that any lines drawn, should be red - of course. On the third line the thickness of the line is set and like we did with the canvas backing store we multiply with the scale factor. Remember that a canvas on the iPad Mini Retina is twice the size of what it's displayed as (yep, yep). If we didn't multiply the line thickness if would actually display at 2.5..and that would be stupid because we want it to look the same everywhere, except it will look sharper on retina displays.

@ctx.moveTo e.touches[0].pageX * @scaleFactor, e.touches[0].pageY * @scaleFactor
#...
@ctx.lineTo e.touches[0].pageX * @scaleFactor, e.touches[0].pageY * @scaleFactor

The same multiplication also needs to happen every time we either move the drawing pointer or draw a line. If we didn't do this the line would be drawn with an offset of the finger or the cursor - again, we don't want that. And that's it, ha!

Other stuff

For mouse mode, it's also a good idea to remove the «mosemove» listener when the move isn't pressed. This way you won't get a bunch of events firing all over the place when you really don't need them.

That's all folks.

CodeKit 2 - First look

CodeKit 2 was released today. Is this just an iterative release, or is it the harbinger of a brighter day for web-developers?

What is CodeKit

CodeKit helps you build websites faster and better.

Bryan Jones, the developer of CodeKit defines it as a tool for helping web-developers build web-sites faster. Or in his own specific humorous words: "It's like steroids for web-developers". He's actually correct, it is a tool which will in most cases significantly improve your performance regarding creating web stuff. 

CodeKit is a web-site build tool, kinda like Grunt, Gulp and others. What separates CodeKit from the other tools listed is that CodeKit is a visual tool, a proper Mac application. It does not require any knowledge of the terminal, writing JSON, installing Ruby gems or Node programming. It just works!

If you're a web-developer and haven't yet discovered CodeKit you have been missing out! Today, the next generation - CodeKit 2 is out. I have been beta-testing it for some time now, both in smaller and larger projects. These are my initial thoughts.

The updated UI

The UI of CodeKit is in a word, new. It is also in my opinion significantly better.

The main file view of CodeKit is now using a tree view. This means that all of your files are organized into folders. This makes for a much more clean presentation compared to the old "show all files in a list" approach.

Tip: You can have CodeKit 2 ignore entire folders by right-clicking on them and choosing "skip this folder". Great for Node projects!

CodeKit 2 features a new project panel (see the top image), which you get at by clicking the big-ass button in the top left corner. Yep, you can customize how the button looks for individual projects. All the functionality from the first CodeKit is still there, you can add new projects, switch them on and off and handle your Compass stuff from the contextual menu button, and now you can add a Zurb Foundation Project as well.

cont_menu.png

There's a lot of new UI stuff in CodeKit 2, all of them improvements in my opinion.

Languages, features and frameworks

The first version of CodeKit did support quite a slew of languages, cross-compilers and external tools. You never needed any knowledge of how to install any of these languages when using CodeKit, they where just there - build in. This is also the case for CodeKit 2, with some welcome additions.

New languages

langs.png

CodeKit 2 adds support for TypeKit and Markdown as well as Autoprefixer, a tool that will add missing user-agent prefixes to your CSS code.

Zurb Foundation and Susy are now supported on the framework side of things. Great for building responsive web-sites.

Also worth mentioning is the addition of CoffeeLint, which will lint your CoffeeScript files before compiling them, making it that much harder to mess up.

Source maps ftw

CodeKit 2 has build in support for source maps, that simply works!

If you ever have tried to set up source maps, you know that it can be quite a pain in the behind to configure correctly. CodeKit 2 performs all that nasty setup for you. This allows you to concentrate on using source maps instead of pulling your hair out trying to make it work correctly. Source maps are supported for CoffeeScript, SCSS, Less, TypeScript and Uglify.js.

The new assets panel

Handling external dependencies like jQuery, Modernizr and Bootstrap can be quite a chore when working on larger projects. Things gets updated, you have to hunt for each individual library on the web and manually check which is the most up to date version and so on.

CodeKit 2 has build in support for Bower, a package manager for the web. Opening the «Assets» panel allows you to browse, search for and install Bower packages. There is also an easy way to update your packages when a new version is available. I particularly like the feature which allows me to update all my installed components in one click.

Hooks

The Hooks feature is exactly what you think it is. It allows you to create a hook with a set of rules and have it  execute a shell-script or AppleScript when the criteria of the rules are met. In the example above I tell my tests to run when any file has changed in the "kit" folder. This feature could for instance allow you to integrate with other command line tools.

Nifty server-refreshing

This is perhaps the flagship feature of CodeKit 2. It does live reload like before, but now it does so much more. CodeKit 2 will fire up a simple HTML server and give you an URL which you can navigate to on any device on your local network. When you change any of the files in your project CodeKit 2 will automagically update the site on all of the devices, even the remote ones like an iPhone.

Consider the following; You are creating a site which should run on mobile devices, desktop devices and a refrigerator (it could happen!). When you change something in your project, CodeKit 2 will compile it and refresh ALL of your connected devices, including remote devices. This is not only awesome, but it will save you a lot of time and frustration when testing on multiple devices.

But, what if your project needs more than a simple HTML server, like a local Node server running on port 3000?

Support for Node / Wordpress / external servers

CodeKit 2 can expose remote servers by piping their requests through it's own server maintaining the ability to update all your devices when something changes. You simply configure the project with a remote server, in my case a Node server running on port 3000 and when you change for instance a SCSS file in the Node project, the site automatically updates on your Mac, iPhone, iPad and refrigerator. Amazeballs!

Conclusion

There's much more new stuff in CodeKit 2, like the «libsass» compiler which makes compiling your SCSS files insanely much faster and project level settings, a new settings file-format which works better for teams and so on.

So, is CodeKit 2 great?

Let me put it to you this way. 
There is a class of applications where the care and attention to detail permeates ever nook and cranny. It surprises you in positive ways and does stuff which makes you smile. CodeKit 2 is clearly such an application in my opinion.

CodeKit 2 is targeted at a specific audience - web developers. If you find yourself being part of that audience you should consider CodeKit 2, it might just make your day brighter.

CodeKit 2 is available from http://incident57.com/codekit/

Quick-tip: Web performance demos

A while back I wrote some performance demos for a presentation I was doing. The demos go into some depth for the following topics:

  • JavaScript download and thread blocking
  • JavaScript libraries and memory usage and execution times compared to pure DOM
  • Paints, invalidation and memory usage when creating layers

All of the demos include an in depth explanations on GitHub, so I won't bother rehashing this here. You can find the demos here: https://github.com/jornki/performance

Performance demos on GitHub

Performance demos on GitHub