Site Overlay

Guide: Use JavaScript in Swift

NOTE: This guide may not be compatible with Swift 4

JavaScript is a great way to give your app a plugin system. Apple doesn’t allow downloading of code into your app, but they do allow downloading Javascript, which means you can create downloadable plugins. In this guide, we will create a class called Plugin which executes a JavaScript file, and provides a bridge to send messages between the JavaScript and your app.

TL/DR: Get the full code here.

What's needed:
– Need to know the Swift 3 language

Create the Plugin.swift file
This will be our starting point.

public class Plugin {

}

Create an initialiser
Our initialiser will take a file URL to a JavaScript file to execute.

class Plugin {

    public init(file : URL) {

    }

}

Create the JavaScript context
The Javascript context contains all the variables and functions used by the Javascript code. To create one, we first import the JavaScriptCore framework, and then create a JSContext.

import JavaScriptCore

public class Plugin {

    var ctx : JSContext!

    public init(file : NSURL) {

        // Create Javascript context
        ctx = JSContext() 
        ctx.name = "My Javascript Context"

    }

}

The name property of JSContext is only used for debugging purposes. If you connect your device to a Mac and then open the Develop menu in Safari, you can access the Javascript context under this name. You can then run code in this context in real-time, just like you can with the Web Inspector.

Create function to pass messages to the plugin
Our Javascript plugins need a way of sending messages between the host app and the plugin. In this example, we will use a similar method to HTML5’s Web Workers, which is to provide a postMessage function for the sender and an onmessage handler for the receiver. First let’s create our sender from the host app to the plugin:

public class Plugin {

    ...

    /** Sends a message to the plugin */
    public func postMessage(data : Any) {

        // Find the plugin's onmessage handler
        let handler = ctx.globalObject.forProperty("onmessage")

        // Send message
        handler?.call(withArguments: [data])

    }

}

There’s quite a bit going on in this small function. Let’s have a look:

Line 6: Our data format is Any. This allows anything to be passed as the message, including numbers, strings, dictionaries, and even your own custom classes.

Line 9: We find a reference to the Javascript’s onmessage handler function, assuming the Javascript code created one. All the Javascript’s global variables and functions will be available via ctx.globalObject. If a property isn't found, it will return a JSValue that is "undefined".

Line 12: We call the handler function. call(withArguments:) takes an array of arguments, so we pass in the supplied data as the first argument.

Add a handler for messages coming in from Javascript to the host app
We can define a public variable to hold the callback block. The host app would then set it to it’s handler block.

import JavaScriptCore

public class Plugin {

    var ctx : JSContext!

    public var onmessage : ((_ data : Any?) -> Void)?

    ...

We use an underscore so that the word "data" doesn't have to be used when calling the function later.

Expose a function to the Javascript which passes messages back to the host app
In order to do this, we will create a block and expose it to Javascript as a function.

    public init(file : NSURL) {

        // Create Javascript context
        ctx = JSContext() 
        ctx.name = "My Javascript Context"

        // Create postMessage code block that will get exposed to Javascript as a function
        let postMessageBlock : @convention(block) (AnyObject?) -> Void = { [weak self] (data) in 

            // Pass message to host app
            self?.onmessage?(data)

        }

        // Expose the postMessage code block as a Javascript function
        ctx.globalObject.setValue(unsafeBitCast(postMessageBlock, to: AnyObject.self), forProperty: "postMessage")

    }

There is a lot of funny stuff going on here, but this is because the JavaScriptCore library is written in Objective-C and is not yet 100% compatible with Swift. Let's have a look:

Line 8: @convention(block) converts the code block from a Swift Closure into an Objective-C Block.

Line 8: [weak self] tells Swift that our reference to self used in the block should be a weak reference. Since this block of code is going to get retained by JavaScriptCore, we don’t want any strong references to our own class, since it would cause a retain cycle. All it means for our app is that self could be null at some point, so we tell it to ignore nulls by using the ? operator.

Line 11: When Javascript calls this block, we simply pass the message data back to the host app by using the onmessage variable.

Line 16: More funny stuff here. Our code block is now in Objective-C format which JavaScriptCore requires, but in order to set it as a property we need to use the setValue(forProperty:) function. But that function takes the value as an AnyObject, which is a Swift type! So we need to trick the Swift compiler into passing the Objective-C block to the function and ignore the type. That’s where unsafeBitCast comes in. It leaves the raw data unchanged, but tells the compiler that it’s actually in a different format. This function should never be used unless you are 100% sure that both data formats are exactly the same in memory! In our case, we’ve already converted it to Objective-C format by using @convention(block), so we can safely do this.

Execute the Javascript
Now the last thing we need to do, is to actually run the Javascript code! This is quite simple:

    public init(file : NSURL) {

        ...

        // Read the Javascript code into a string
        if let code = try? String(contentsOf: file, encoding: .utf8) {

            // Execute the Javascript code
            ctx.evaluateScript(code, withSourceURL: file)

        }

    }

We first fetch the contents of the file, decoded as a UTF8 String, and then put it in the code variable. If that works, we then execute the Javascript string.

Done! You can download the completed class here.


Next: Test it out!