How To Example: Extend a iOS Using JavascriptCore As A Macro Engine

Date Published:
Last Modified: by

Introduction

JavascriptCore is a framework that offers the ability for a iOS application to interact with javascript code. Primarily used for cross platform code sharing, it can also be used to extend a iOS application with macro capabilities. This framework offers a world of possibilities for extending any iOS application.

The Demo Application

The demo application accompanying this article is located here: JavascriptCore Macro Engine. The application reuses the custom process control blog article, and demo application.

The demo app demonstrates how to offer the ability to change the progress indicators through javascript. The user selects a button, the button’s action invokes a javascript function that updates the progress values by calling a swift block, which updates the progress values. The application also allows the user to modify the javascript functions by selecting the “Edit” button.

JavascriptCore Overview

JavascriptCore framework was added to iOS in release 7 and to OS X with the Mavericks release. The framework offers four classes: JSContext, JSManagedValue, JSValue, and JSVirtualMachine, along with the JSExport protocol.

JSContext represents a Javascript execution environment. You will need to create and use a context to evaluate Javascript. Each JSContext object belongs to a JSVirtualMachine. A JSVirtualMachine can supports multiple JSContexts. This allows JSValues to be passed between contexts that share the same JSVirtualMachine.

JavascriptCore APIs are thread safe. Which means that all API calls to the same virtual machine will wait. To get concurrency you will have to instantiate multiple JSVirtualMachines.

Memory retain cycles can happen when you store a JSValue object into a swift or objective-c object that is in turn exported back to Javascript. In this case Apple recommends that you use a JSManagedValue instead.

JSContext provides two methods for executing javascript: evaluateScript, and evaluateString:withSourceURL. Essentially, it takes the passed javascript code and executes it in the context. Any error encountered are passed back to swift/objective-c through a exceptionHandler interface.

You will need to register your own exception handler. The handler is a block that is passed the context where the error happened along with a JSValue containing a string describing the error.

A JSContext has access to javascript global object. Use the methods objectForKeyedSubscript and SetObject:forKeyedSubscript to interact with the global object.

JSValue is the workhorse of the framework. A JSValue instance represents a Javascript value. This class facilitates the conversion of basic types like numbers and string between Javascript and Swift/Object-C. Hence the class has numerous methods for creating/converting types. This is mostly automatic and follows the following rules: 1. NSDictionary objects and Swift dictionaries become Javascript objects 2. NSArray objects or Swift arrays become Javascript arrays. 3. Objective-C blocks become javascript function objects 4. Custom objects are represented with a Javascript wrapper that uses the JSExport protocol to pick and choose which native object’s properties and methods are exported to Javascript.

See the Apple JSValue Documentation for a complete description of the type conversions._

Use the JSExport protocol to make custom native objects available to Javascript. You will need to derive a new protocol from JSExport and list the property or methods that you want to make available.

Macro Engine

The demo application uses a custom MacroEngine class. This class follows the singleton pattern:

/// Class that manages the macro engine.  It has a static sharedInstance
/// property that should be used.
class MacroEngine {

    /// Static property for the one global instance of the MacroEngine
    static let sharedInstance: MacroEngine = MacroEngine()


    /// JavascriptCore context instance that the macro engine is using
    let context: JSContext = JSContext()


    init() {
        setupExceptionHandler()
    }

    .
    .
    .

    /// Private method used to setup the exception handler.  The handler
    /// presents the error in a alert view
    private func setupExceptionHandler() {

        context.exceptionHandler = { context, error in
            let alert = UIAlertController(title: "Javascript Evaluation Error", message: error.toString(), preferredStyle: .Alert)
            alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))

            UIApplication.sharedApplication()
                .keyWindow?
                .rootViewController?
                .presentedViewController?
                .presentViewController(alert,
                                       animated: true,
                                       completion: nil)
        }
    }
}

This class is pretty light weight at this point. It creates a JSContext instance, registers a exception handler, and provides methods a basic protocol for registering objects with the context. The JSContext can be accessed directly by using the context property. While the exception handler creates a UIAlertView to display the error the context.

/// Protocol used by the MacroEngine to setup the macro objects for a class.
protocol MacroEngineSupport {
    func addMacroSupport(engine: MacroEngine)
}

The protocol MacroEngineSupport is used by native objects to register with the MacroEngine. It is a single method that is called on the native instance by the global MacroEngine instance. So something in the application will need to call the MacroEngine method addObject:

    /// Method used to add a object to the macro engine.  This method
    /// uses the MacroEngineSupport property to call the object's
    /// macro initialization method.
    ///
    /// - parameter object: Object supporting the MacroEngineSupport
    /// protocol
    func addObject(object: MacroEngineSupport) {
        object.addMacroSupport(self)
    }

This method simple calls the protocol method on native instance that is passed in. It is the native instance’s responsibility to register everything it wants exported to the Javascript context.

The insertBlockAsObject method is used to export Objective-C blocks to the JSContext:

    /// Call this method is inject a callback block into the javascript
    /// context.  This will allow any javascript code to call this block
    ///
    /// - parameter block:  Objective-C block
    /// - parameter key: String to use as the object name in the javascript
    /// context.
    func insertBlockAsObject<BlockType>(block: BlockType, key: String) {

        context.setObject(unsafeBitCast(block, AnyObject.self),
                          forKeyedSubscript: key)
    }

This method takes the passed block and key and calls the JSContext method setObject. On success the block will be accessible to Javascript code using the passed key.

The final method in MacroEngine is used to register Javascript functions as global objects that can be executed by native code:

    /// This method is called to register a javascript method.  It returns the
    /// JSValue to use to call this method from swift.
    ///
    /// - parameter javascript: String containing the javascript for this
    /// method.
    /// - parameter key: String containing the javascript context name
    func setMethod(javascript: String, key: String) -> JSValue! {
        let script = "var \(key) = \(javascript)"
        context.evaluateScript(script)

        return context.objectForKeyedSubscript(key)
    }

This method builds a Javascript function declaration using the Javascript and key passed in. This method is used by the MacroMethod class:

/// Class that is used to implementing javascript methods.  Use an instance
/// to manage and execute a javascript method.
class MacroMethod {

    private let engine = MacroEngine.sharedInstance
    private var jsValue: JSValue!

    /// Property that contains the javascript method
    var javascript: String {
        didSet {
            setMethod()
        }
    }


    /// The name in the javascript context of the method
    let key: String


    init(javascript: String, key: String) {
        self.key = key
        self.javascript = javascript

        setMethod()
    }


    /// This method is used to invoke the javascript method.
    ///
    /// - parameter [params]:   The parameters for the javascript method.
    ///
    /// - return: A JSValue object result returned by the javascript
    /// method
    func call(params: AnyObject...) -> JSValue! {
        return jsValue.callWithArguments(params)
    }


    /// A private method used to configure the javascript method
    /// into the macro engine's context
    private func setMethod() {
        jsValue = engine.setMethod(javascript, key: key)
    }
}

This class is designed to facilitate maintaining Javascript functions for native code. It maintains two private properties that link it to the proper JSContext and JSValues. It also has a public method that contains the Javascript as a String. The Javascript JSValue is updated each time this property is updated. The Javascript is exported using the MacroEngine setMethod.

The Javascript function is called using the call method. This method uses variable number of parameters to package up the Javascript’s parameters. The array of parameters are converted automatically by the JSValue class. The return parameter is a JSValue that will need to be converted.

Changing Progress Indicator Values

The demo application uses a global object to store the progress values for the three progress indicators. The class is called Thing:

/// Class used to contain the progress values.  The class registers methods
/// with the MacroEngine that support setting the values through calling
/// Objective-C blocks.
class Thing: MacroEngineSupport {


    /// static proerty used to retrieve the one global instance of this class
    static var sharedInstance: Thing = Thing()


    /// property used for setting the delegate callback
    var delegate: ProgressUpdate?


    /// property containing the inner ring's progress value
    var innerProgress: Float {
        didSet {
            if innerProgress < 0.0 {
                innerProgress = 0
            } else if innerProgress > 10.0 {
                innerProgress = 10.0
            }

            delegate?.innerProgressUpdate(innerProgress)
        }
    }


    /// property containing the middle ring's progress value
    var middleProgress: Float {
        didSet {
            if middleProgress < 0.0 {
                middleProgress = 0.0
            } else if middleProgress > 10.0 {
                middleProgress = 10.0
            }

            delegate?.middleProgressUpdate(middleProgress)
        }
    }


    /// property containing the outer ring's progress value
    var outerProgress: Float {
        didSet {
            if outerProgress < 0.0 {
                outerProgress = 0.0
            } else if outerProgress > 10.0 {
                outerProgress = 10.0
            }

            delegate?.outerProgressUpdate(outerProgress)
        }
    }


    init(innerProgress: Float = 0.0, middleProgress: Float = 0.0, outerProgress: Float = 0.0) {
        self.innerProgress = innerProgress
        self.middleProgress = middleProgress
        self.outerProgress = outerProgress

        // Add to macro enginer
        MacroEngine.sharedInstance.addObject(self)
    }


    /// MacroEngine protocol method used to register the blocks used
    /// by javascript.
    ///
    /// - parameter engine: An instance of the MacroEngine class to
    /// register with
    func addMacroSupport(engine: MacroEngine) {
        engine.insertBlockAsObject(setInnerProgress, key: "setInnerProgress")
        engine.insertBlockAsObject(getInnerProgress, key: "getInnerProgress")

        engine.insertBlockAsObject(setMiddleProgress, key: "setMiddleProgress")
        engine.insertBlockAsObject(getMiddleProgress, key: "getMiddleProgress")

        engine.insertBlockAsObject(setOuterProgress, key: "setOuterProgress")
        engine.insertBlockAsObject(getOuterProgress, key: "getOuterProgress")
    }
}

The class is pretty straight forward. It has three properties, one for each ring: inner, middle, and outer. It uses the delegator pattern for communicating changes to other native objects. You could use the JSExport protocol to export this object to Javascript and call out the getter/setters for each property. But, in swift, you lose the ability to detect changes. Swift does not currently allow you to simultaneously declare getters/setter alongside willSet/didSet.

To work around this the Thing class calls the MacroEngine and registers blocks for getting and setting each of the progress properties:

// Javascript callbacks for getting/setting innerProgress
private let setInnerProgress: @convention(block) Float -> Void = { newValue in
    Thing.sharedInstance.innerProgress = newValue
}
private let getInnerProgress: @convention(block) Void -> Float = {
    return Thing.sharedInstance.innerProgress
}

Javascript code just needs to call these functions to access the progress values. Once a value is changed, the delegate is informed by the ProgressUpdate protocol:

/// Delegate protocol used to notify when a progress value has been updated
protocol ProgressUpdate {
    func innerProgressUpdate(progress: Float)
    func middleProgressUpdate(progress: Float)
    func outerProgressUpdate(progress: Float)
}

Invoking Javascript Functions

The demo application’s main view controller is ViewController. This class registers four macro functions with the MacroEngine by calling its registerJavascriptMethods in viewDidLoad:

    /// A private method used to register the javascript methods
    /// with the macro engine
    private func registerJavascriptMethods() {
        for file in javascriptFiles {
            macros.append(loadJavascriptFile(file))
        }
    }

This method loops through the javascriptFiles array of filenames and calls the loadJavascriptFile method:

    /// A private method that is called to load contents of the
    /// javascript file included in the resource bundle of the application
    private func loadJavascriptFile(key: String) -> MacroMethod {
        guard let filePath = NSBundle.mainBundle().pathForResource(key, ofType: "js") else {
            print("Unable to load javascript file \(key).js")
            fatalError()
        }

        let script = try! String(contentsOfFile: filePath,
                                 encoding: NSUTF8StringEncoding)

        return MacroMethod(javascript: script, key: key)
    }

This method loads the Javascript file from he application bundle and create a MacroMethod instance that is used by the view controller for calling and changing the Javascript function.

Each Javascript file follows this template:

function() {
    var currentInner = getInnerProgress();
    var currentMiddle = getMiddleProgress();
    var currentOuter = getOuterProgress();

    setInnerProgress(currentInner + Math.random());
    setMiddleProgress(currentMiddle + Math.random());
    setOuterProgress(currentOuter + Math.random());
}

The function calls the native blocks exported by the global Thing instance to retrieve each progress value, and increment or decrement by random amounts.

The view has four buttons. Each button is linked to a Javascript “macro” by the IBAction:

    /// IBAction used by the progress indicator buttons
    @IBAction func executeMacroButton(sender: UIButton) {

        // Call the javascript method
        macros[sender.tag].call()

        // Load the javascript for the button just executed
        codeView?.text = macros[sender.tag].javascript
    }

Each button has its tag value set to correspond with the proper Javascript “macro.”

Editing Javascript

The main view controller has a “Edit” button. This button segues to the MacroEditViewController. This view controller has a UITextView and a UISegmentedControl for displaying/editing each one of the four “macros.” The UITextView displays the Javascript code using TextKit to display the line numbers on the left. This is an interesting problem in and of itself.

The MacroEditViewController is passed the four instances of the MacroMethod class. The MacroMethod instances handle changing the “macros” when they are changed.

/// View controller used for editing the javascript macros
class MacroEditViewController: UIViewController {


    @IBOutlet weak var macroSelector: UISegmentedControl!
    @IBOutlet weak var codeView: CodeView!


    var macros: [MacroMethod] = []
    var lastSelectedIndex: Int = 0


    deinit {
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }


    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        macroSelector.selectedSegmentIndex = 0
        codeView.text = macros[0].javascript
        lastSelectedIndex = 0
    }


    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


    @IBAction func allDone(sender: AnyObject) {
        codeView.textView.resignFirstResponder()
        saveCurrentMacro()

        self.dismissViewControllerAnimated(true, completion: nil)
    }


    @IBAction func selectedMacro(sender: AnyObject) {
        codeView.textView.resignFirstResponder()

        saveCurrentMacro()
        lastSelectedIndex = macroSelector.selectedSegmentIndex
    }


    private func saveCurrentMacro() {
        macros[lastSelectedIndex].javascript = codeView.text!
        codeView.text = macros[macroSelector.selectedSegmentIndex].javascript
    }
}

JavascriptCore In Relation to WKWebView

Apple added WKWebView in iOS 8. It offers a much faster Javascript execution environment the UIWebView. Unfortunately is uses a different scheme for supporting Javascript. Here the purpose of the Javascript is to interact with HTML pages. It is a different purpose then what we are exploring here so I do not think it is that big of an idea.

In Conclusion

Using JavascriptCore is pretty straightforward to use as a way to extend a iOS application. Two areas that will require extensive work is editing and debugging Javascript code. The exception handle offers little in information in debugging capabilities. While the standard UITextView is tedious for editing Javascript code.

Rodger Higgins is the founder of Spazstik Software, LLC. He has created StackCalc, The Visual Touch Calculator and SPZTracker.

Recent Articles

A Reusable Observer Protocol Written In Swift

One design pattern that I use a lot is the observer pattern. The observer pattern is used when you have an object that needs to notify a list of objects that state changes have happened. This article discusses a reusable component, in Swift, I developed to speed up my development process.


Read More...
How To: Support User Editable Python Macros In A I Os Application

Last month I published a article on how to use JavascriptCore for extending a iOS application with macro support. While Javascript has many uses, as a way for application customization, it would not be my first choice.

A better choice to me would be a language like Python. Being curious, I wondered what it would take to to use Python. This article discusses what I found.


Read More...
How To Example: Extend A I Os Using Javascript Core As A Macro Engine

JavascriptCore is a framework that offers the ability for a iOS application to interact with javascript code. Primarily used for cross platform code sharing, it can also be used to extend a iOS application with macro capabilities. This framework offers a world of possibilities for extending any iOS application.


Read More...
How To: Custom I Os Activity Tracker View Using Ca Layers

The Apple Watch shipped with a captivating activity tracker. The center piece is a really cool spiral animation scheme showing the amount of activity during the day. This image is also shown on the matching iPhone Activity App. I have always wanted to see what it would take to implement this myself. The examples that I see typically use a custom drawRect override, but I always wanted to see what it would take to do with CAShapeLayers.

Implementing a 0-100% control is straight forward when using CAShapeLayer. But how do you implement a progress indicator that support progress values greater then 100%? This How To discusses a solution that I came up with along with it’s potential limitations.


Read More...
How To Display Custom Content On A External Screen From A I Os Device

Being able to display content on a external screen or device is a great capability to add to a iOS application. Especially how easy it is. This article will show the step required to to do this.


Read More...

Follow us on

Articles by published month

Articles by subject matter

Rails Thor Compass Susy Modernizr Rspec Capybara Bettererrors Railspanel Aws Rack Railscasts Http Aws-elastic-beanstalk Ruby-on-rails Rack-rewrite Http-response-codes Pow-amazon-route-53 Stackcalc Iphone Ios Mobile Application Skeumorphic Dns Web-site Elastic-beanstalk Elastic-ip Elastic-load-balancer Tutorial Howto Javascript Javascriptcore Macro Example Swift Design-patterns Observer Python Macros Alamofire External-screen-support Apple-watch Activity-tracker Office Status S100 Cloud Astronomy Picture Mars Apple Usb-c Leonard-nimoy William-shatner Geotag Gps Spztracker Geotagging Photos Secret Marketing Watch Watch-repair Head-transplants Perception Diabeties Sugar Health Rosette-nebula News Spock Comet-lovejoy

Click here to receive free tips and tutorials!

This web site uses javascript exclusively for automating html elements. Please enable javascript to fully experience the features offered on this site.