Introduction to JavaScriptCore Framework in iOS Development

If you’ve been working with iOS for a while, you might have encountered a situation where you had to execute some JavaScript code. Chances are that you’ve found the – stringByEvaluatingJavaScriptFromString: method, which is quite limited in its capacities.

javascriptcore

The JavaScriptCore Framework was introduced in iOS 7.0. This open source framework has been available in MacOS for a while, and it’s has not been well documented so far and your best chance is to have a look at the headers. Despite all that, it’s a very powerful tool to have under your belt, so I’ll get you started with it.

 

The Playground

In order to execute a script, you need two things, a JSVirtualMachine, and a JSContext. You can have multiple Virtual Machines, each one holding multiple Contexts and by creating many Virtual Machines, you can execute JavaScript in multiple threads.

    JSVirtualMachine *virtualMachine;
    JSContext *context;
    virtualMachine = [[JSVirtualMachine alloc] init];
    context = [[JSContext alloc] initWithVirtualMachine:virtualMachine];

You can access the properties in the JavaScript global object through object subscripting, treating the context as if it were a NSDictionary:

context[@"x"] = @31337;

This will create a global variable x, with a value of 31337. This also works the other way around, but every value you retrieve from the context will be an instance of JSValue; this class provides method to convert the value to primitives or Foundation objects:

    JSValue *value = context[@"x"];
    NSNumber *x = [value toNumber];
    NSLog(@"%@",x);
    NSString *xString = [value toString];
    NSLog(@"%@",xString);
    double xDouble = [value toDouble];
    NSLog(@"%f",xDouble);

Output:

2014-05-26 10:13:49.247 JavaScript[689:90b] 31337

2014-05-26 10:13:49.248 JavaScript[689:90b] 31337

2014-05-26 10:13:49.248 JavaScript[689:90b] 31337.000000

 

Talk to Me

If you want to do anything meaningful in JS you’ll have to make some function calls. In order to “inject” code into the context, you must use the context’s evaluateScript method.

[context evaluateScript:@"var globalX; function firstFunction() { globalX = 'hello'; }"];

After this method has ended, there will be a global var in the context and a function called firstFunction, but there’s no value stored in globalX yet, so you must call the function:

    [context evaluateScript:@"firstFunction()"];
    JSValue *globalX = context[@"globalX"];
    NSLog(@"%@",globalX);

Output

2014-05-26 10:13:49.251 JavaScript[689:90b] hello

If the method returns some value, you’ll can receive it as an JSValue:

    [context evaluateScript:@"function secondFunction() { return 'world'; };"];
    JSValue *secondFunctionValue = [context evaluateScript:@"secondFunction();"];
    NSLog(@"%@",[secondFunctionValue toString]);

Output

2014-05-26 10:13:49.252 JavaScript[689:90b] world

There are times when you need to pass some arguments into the JS function. When you pass objects to the context, the following equivalence table is followed (taken from JSValue.h)

<span style="color: #008000;">//   Objective-C type</span>
<span style="color: #008000;">// --------------------</span>
<span style="color: #008000;">//         nil</span>
<span style="color: #008000;">//        NSNull</span>
<span style="color: #008000;">//       NSString </span>
<span style="color: #008000;">//       NSNumber</span>
<span style="color: #008000;">//     NSDictionary</span>
<span style="color: #008000;">//       NSArray</span>
<span style="color: #008000;">//        NSDate</span>
<span style="color: #008000;">//       NSBlock *</span>
<span style="color: #008000;">//          id **</span>
<span style="color: #008000;">//        Class ***</span>
<span style="white-space: pre-wrap; color: #008000;">|   JavaScript type</span>
<span style="color: #008000;">+---------------------</span>
<span style="color: #008000;">|     undefined</span>
<span style="color: #008000;">|        null</span>
<span style="color: #008000;">|       string</span>
<span style="color: #008000;">|   number, boolean</span>
<span style="color: #008000;">|   Object object</span>
<span style="color: #008000;">|    Array object</span>
<span style="color: #008000;">|     Date object</span>
<span style="color: #008000;">|   Function object *</span>
<span style="color: #008000;">|   Wrapper object **</span>
<span style="color: #008000;">| Constructor object ***</span>

In this case you can call a JS function with the callWithArguments:

    [context evaluateScript:@"var sum = function (a, b) { return a + b; }"];
    JSValue *sum = context[@"sum"];
    JSValue *sumResult = [sum callWithArguments:@[ @31000, @337 ]];
    NSLog(@"%@", [sumResult toNumber]);

Output

2014-05-26 10:13:49.252 JavaScript[689:90b] 31337

You can pass some JSValue as argument too, not just Foundation objects:

    JSValue *value = [JSValue valueWithInt32:337 inContext:context];
    sumResult = [sum callWithArguments:@[@31000, value]];
    NSLog(@"%@", [sumResult toNumber]);

Output

2014-05-26 10:13:49.253 JavaScript[689:90b] 31337

If you take a look into the equivalence table, when you pass a id pointer to JS, it will be converted to a Wrapper Object, this is helpful when you want to pass a custom object and make its properties available in JS, you can do it like this:

Create a protocol conforming the JSExport and include every property you want to make available in the context.

//  ITXExposedObject.h
@import Foundation;
@import JavaScriptCore;
@protocol ITXObjectExports &lt;JSExport&gt;
@property (strong, nonatomic) NSString *exposedProperty;
@end
@interface ITXExposedObject : NSObject &lt;ITXObjectExports&gt;
@property (strong, nonatomic) NSString *exposedProperty;
@property (strong, nonatomic) NSString *hiddenProperty;
@end
//  ITXExposedObject.m
#import "ITXExposedObject.h"
@implementation ITXExposedObject
- (NSString*)description {
    return [NSString stringWithFormat:@"%@ %@",self.exposedProperty, self.hiddenProperty];
}
@end

As you can see there’s a exposedProperty property, which will be available, while the hiddenProperty won’t be disturbed. You can use the object as usual in Objective-C

    ITXExposedObject *exposedObject = [[ITXExposedObject alloc] init];
    exposedObject.exposedProperty = @"Hello";
    exposedObject.hiddenProperty = @"nurse";
    context[@"exposedObject"] = exposedObject;
    NSLog(@"%@",exposedObject);
    NSLog(@"%@",context[@"exposedObject"]);

Output

2014-05-26 10:13:49.254 JavaScript[689:90b] Hello nurse

2014-05-26 10:13:49.254 JavaScript[689:90b] Hello nurse

You can modify the object from Objective-C code, the changes will be reflected in JS:

    exposedObject.exposedProperty = @"Bonjour";
    exposedObject.hiddenProperty = @"infirmière";
    NSLog(@"%@",exposedObject);
    NSLog(@"%@",context[@"exposedObject"]);

Output

2014-05-26 10:13:49.255 JavaScript[689:90b] Bonjour infirmière

2014-05-26 10:13:49.255 JavaScript[689:90b] Bonjour infirmière

You can modify the exposedProperty from JS, but you can’t change the hiddenProperty:

    [context evaluateScript:@"exposedObject.exposedProperty = 'Hola'; exposedObject.hiddenProperty = 'enfermera';"];
    NSLog(@"%@",exposedObject);
    NSLog(@"%@",context[@"exposedObject"]);

Output

2014-05-26 10:13:49.255 JavaScript[689:90b] Hola infirmière

2014-05-26 10:13:49.256 JavaScript[689:90b] Hola infirmière

OK, so now I can execute functions from Objective-C.  Can I execute  Objective-C code from JS?

I’m glad you asked!

One thing to remember is that the JavaScriptCore Framework implements the ECMAScript, meaning there’s no AJAX, or other browser-defined features. Fortunately, you can provide functionality from Cocoa to JS through blocks; for example, fetch a file from an URL:

    context[@"stringWithContentsOfURL"] = (NSString*) ^(NSString* urlString) {
        NSError *error = nil;
        NSString *content = [NSString stringWithContentsOfURL:[NSURL URLWithString:urlString]
                                                     encoding:NSUTF8StringEncoding
                                                        error:&amp;error];
        if (error) {
            NSLog(@"stringWithContentsOfURL error: %@",error.description);
        }
        return content;
    };
    NSLog(@"%@",[[context evaluateScript:@"stringWithContentsOfURL('http://www.test.com');"] toString]);

Output

2014-05-26 11:03:19.698 JavaScript[853:90b] <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US">

... (you get the idea)

 

So that’s all folks, I hope I’ve interested you in JavaScriptCore, a tool I’ve found very powerful and helpful… And at last, two important things:

1.- Debugging your JS code can be painful, so any helpful tips of what’s going on are welcome. You can set a handler to receive notifications of exceptions from the context, this will give you a hint of where the problem might lie:

    [context setExceptionHandler:^(JSContext *context, JSValue *value) {
        NSLog(@"%@",value);
    }];

Output:

2014-05-26 11:03:19.704 JavaScript[853:90b] SyntaxError: Expected token ')'

2.- Apple is very clear on what can and what shouldn’t be done with scripts. Acording to this Stackoverflow answer, you cannot download any code from the web. All of your scripts should be included with your bundle if you want to make it into the appstore:

  • * 3.3.2. An Application may not download or install executable code. Interpreted code mayonly be used in an Application if all scripts, code and interpreters are packaged in theApplication and not downloaded. The only exception to the foregoing is scripts and codedownloaded and run by Apple's built-in WebKit framework.

About the Author

Emanuel P. is a native iOS Developer with 5+ years of experience with the iOS SDK. He has a bachelors degree in Computer Systems Engineering.  He loves human and programming languages and movies.

 

Download our Free E-Book and learn how the Product Mindset can benefit your mobile app. 

Through this guide, we want to give you, product owners, product managers, engineers, entrepreneurs and anybody looking to build great digital products, more insights into the importance of user-centric design and development and how to correctly achieve it.


How Product Mindset Can Save your Sinking App

You may also like:

Post Your Comment Here