Skip to content

A light weight wrapper to call Javascript functions / objects from swift

License

Notifications You must be signed in to change notification settings

silverfoxlabs/swiftjs-wrapper

Repository files navigation

swiftjs-wrapper

Introduction:

JavascriptCore is a powerful framework on the macOS/iOS/tvOS platform. Interacting with it through swift leaves much to be desired since it is still an Objective-C framework.

swiftjs-wrapper aims to alleviate some of the boilerplate and add some type safety to interacting with JavascriptCore.

Functionality is focused right now on loading a script from a file and interacting with the objects and functions through the native side.

Supported Platforms:

macOS 10.10+, iOS 11.0+, tvOS 11.0+ are all supported.

Usage:

Load a Javascript file from the Bundle:

Javascript<JavascriptFile> is the main entry point of loading Javascript into a JSContext as well as using the context to call additional functions, so its probably a good idea to keep this object around in memory. You can create as many Javascript<JavascriptFile> instances as you need, each one creates its own context.

Here is an example of how to use Javascript<JavascriptFile> :

static func loader() throws -> Javascript<JavascriptFile> {

        let bundle = Bundle(for: SomeClassYouCreate.self)
        guard let path = bundle.url(forResource: "bundle", withExtension: "js") else {
            throw JSBridgeError.couldNotInitialize
        }

        do {

            let file : JavascriptFile = try JavascriptFile(path: path)
            let loader : Javascript<JavascriptFile> = try Javascript(resource: file)
            return loader
        }
        catch {
            print(error.localizedDescription)
            throw error
        }
    }

After initializing an instance of Javascript<JavascriptFile> call load() to set everything up:

try loader.load()

Great! So we have now loaded our Javascript into a context and everything is all setup. How can we start interacting with the Javascript code? The first step is to conform to JavascriptModule, here is an example:

public struct MyModule : JavascriptModule {

    public func call<FunctionName, ReturnType, Arguments>(name: FunctionName, args: Arguments, onObject: Any?) throws -> ReturnType {

        throw JSBridgeError.couldNotInitialize
    }
    
    public typealias BackingType = JSValue
    public typealias Name = JSBridgeBox<JSBridgeCallType, String>

    public var name: JSBridgeBox<JSBridgeCallType, String> = JSBridgeBox(key: .module, value: "MyModule")
    public var context: JSContext
    public var module: JSValue

    public init(context: JSContext) throws {
        do {
            self.context = context
            guard let _module = context.objectForKeyedSubscript(self.name.value) else {
                throw JSBridgeError.couldNotInitialize
            }

            self.module = _module
        }
        catch {
            throw error
        }
    }
}

Highlight on this for a moment:

public func call<FunctionName, ReturnType, Arguments>(name: FunctionName, args: Arguments, onObject: Any?) throws -> ReturnType {

        throw JSBridgeError.couldNotInitialize
    }

swiftjs-wrapper includes a special generic function with constraints to make calling Javascript functions easy. It is declared in Javascript.swift:

public func call<FunctionName, ReturnType, Arguments, Object>(name: FunctionName, args: Arguments, onObject: Object?) throws -> ReturnType where FunctionName : JSBridgeBox<JSBridgeCallType, String> , Arguments : JSBridgeBox<String, Array<Any>>, Object : JSValue

By throwing an error in our struct's implementation of this function - we want to make sure we call the above version of the function. For our use case we want the default functionality. This constrained version of the function covers most use cases in interacting with the Javascript code, including being able to specify what type action to take on the calling function to Javascript through the use of the JSBridgeCallType:

public enum JSBridgeCallType : String {
    case function = "function"
    case object = "object"
    case module = "module"
}

which allows us to do something like:

        let _name = name.value
        let flatArgs = args.value.compactMap { return $0 }

        switch name.key {
        case .function:
            guard let funk : ReturnType = onObject?.invokeMethod(_name, withArguments:    flatArgs) as? ReturnType else { throw JSBridgeError.failure }
            return funk
        case .module,
             .object:
            guard let object : ReturnType = onObject?.objectForKeyedSubscript(_name) as? ReturnType else { throw JSBridgeError.failure }
            return object
        }

So what does the API interaction look like when actually using this in code? (see swiftjs_wrapperTests.swift):

            let loader = try self.getLoader()
            try loader.load()

            //Call the function "personJSON" , which returns a String (as JSON)
            let caller = try MyModule(context: loader.context)

            let funkName = JSBridgeBox(key: JSBridgeCallType.function, value: "personJSON")
            let args = JSBridgeBox(key: "args", value: [Any]())
            let result : JSValue = try caller.call(name: funkName, args: args, onObject: caller.module)
            let str : String = try result.convert(to: .string)


            guard let _data = str.data(using: .utf8) else { return }
            let person = try JSONDecoder().decode(Person.self, from: _data)

💯

About

A light weight wrapper to call Javascript functions / objects from swift

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages