Add Subscripts to JavaScriptCore Types in Swift

The JavaScriptCore framework was apparently very convenient to use in Objective-C times: you could simply use subscripts to change objects inside the context, like this:

jsContext[@"objectName"][@"property"] = @"hello!";

In Swift, tutorials you find on the web stick to the longer version that underlies the subscript convention here: method calls to -objectForKeyedSubscript and -setObject:forKeyedSubscript:.

For example:

// Getter:
if let variableHelloWorld = jsContext.objectForKeyedSubscript("helloWorld") {
    print(variableHelloWorld.toString())
}

// Setter:
let luckyNumbersHandler: @convention(block) ([Int]) -> Void = { luckyNumbers in
    // ...
}
jsContext.setObject(
    unsafeBitCast(luckyNumbersHandler, to: AnyObject.self), 
    forKeyedSubscript: "handleLuckyNumbers" as (NSCopying & NSObjectProtocol)!)

Here are a couple of tips:

  • You can shorten "foo" as (NSCopying & NSObjectProtocol)! to "foo" as NSString.
  • You don’t need to use unsafeBitCast and can pass Swift blocks directly to a JavaScriptContext.
  • You can write custom Swift extensions for the subscripts.

Swift Subscripts for JSContext and JSValue

Here’s an extension to the two core types that support subscripting. The method signatures of both JSContext and JSValue are a bit different. One takes key: Any!, one takes key: (NSCopying & NSObjectProtocol)!, for example. That’s why we need to duplicate the code – a common protocol abstraction won’t work without producing more than 3x the code. (I tried.)

extension JSContext {
    subscript(_ key: NSString) -> JSValue? {
        get { return objectForKeyedSubscript(key) }
    }

    subscript(_ key: NSString) -> Any? {
        get { return objectForKeyedSubscript(key) }
        set { setObject(newValue, forKeyedSubscript: key) }
    }
}

extension JSValue {
    subscript(_ key: NSString) -> JSValue? {
        get { return objectForKeyedSubscript(key) }
    }

    subscript(_ key: NSString) -> Any? {
        get { return objectForKeyedSubscript(key) }
        set { setObject(newValue, forKeyedSubscript: key) }
    }
}

With that, statements become a lot simpler:

// Setter
jsContext["foo"] = 123

// Getter
print(jsContext["helloWorld"]?.toString())