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!