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())