JavaScript is a nice simple language, used in almost all web browsers. And it is not limited to your browser, it is also ideal for scripting apps. The benefits of using JavaScript as a scripting language are many. JavaScript has a large base of people who have come into contact with it and are used to using it. It is a simple an nice clean language (when separated from browser issues and DOM inconsistencies). And it is very simple to integrate. Here's a flavour of what JavaScript looks like:

function myFunction()
{
    return ("Hello, have a nice day!")
}
document.write(myFunction())

This tutorial shows how to script Cocoa using JavaScript. The code in this tutorial is taken from a simple turtle drawing app that uses JavaScript to control a turtle that draws in an NSView. The final program and source code is available to download here. A screenshot of the final result is below.

For reference there are many very good alternatives to using JavaScript to script Cocoa applications. Some of the best are:

  • F-Script — Awesome light-weight language specifically designed for Cocoa. Easy and powerful to use.
  • RubyCocoa — Cocoa combined with the beautiful Ruby language.
  • PyObjC — A mature and slick integration of Cocoa and Python.

One of the benefits of using JavaScript is that we can utilise the built-in functionality of WebKit, the framework behind Safari. This is only available in Mac OS X 10.3.9 or better. After adding the WebKit framework to your project getting JavaScript running is simple.

Your scripts will run inside the environment of a WebView. The first steps to running JavaScript are to setup and initialise a WebView. This can be done simply, as below. First we create a new WebView using [[WebView alloc] init], however the scripting system is not initalised until the WebView loads.

The remaining initalisation happens in awakeFromNib, we set the load delegate for the WebView and load an empty string. The delegate method then finishes the initalisation by recording the, now valid, windowScriptObject.

- (id)init
{
    [super init];
    webView = [[WebView alloc] init];
    return self;
}

- (void)awakeFromNib
{
    [[webView mainFrame] loadHTMLString:@"" baseURL:NULL];
    
    scriptObject = [webView windowScriptObject];
    [scriptObject setValue:turtleView forKey:@"turtle"];
}

...

That is all the initialisation that is needed to be able to begin to use JavaScript.

If desired it would be possible to load up a real HTML page, instead of a blank string, this could contain javascript code as a form of bootstrapping or a base library.

To access JavaScript from Objective-C is now straight-forward. Using evaluateWebScript: we use the scriptObject to run any JavaScript. An alternative method is to call JavaScript functions directly using callWebScriptMethod:withArguments:, this is more useful if you want many actions to call different methods in the same code.

id result = [scriptObject evaluateWebScript:@"14*Math.sin(0.3)"];
id greeting = [scriptObject callWebScriptMethod:@"sayHelloTo" withArguments:[NSArray arrayWithObject:@"Will"];

It is also possible to set values in the script object from Objective-C so that we can pass values into the script. The JavaScript bridge transparently converts scalars, numbers, strings, arrays, and null to and from their Cocoa counterparts, int, float, etc..., NSNumber, NSString, NSArray, and NSNull. Other objects are wrapped and unwrapped as WebScriptObjects

[scriptObject setValue:turtleView forKey:@"turtle"];
TurtleScript, my example program, uses two views, turtleView controls the turtle and is accessed from JavaScript, the other is a NSTextView. The controller object then simply sets the turtleView to the key "turtle" in the WebScriptObject and calls evaluateWebScript: with the contents of the NSTextView. The exact code can be seen at the bottom of this page.

Accessing Objective-C is almost as simple. First we have to give JavaScript an Objective-C object to play with; this is done with setValue:forKey: shown in the above example. Once you have passed your object to JavaScript it will be wrapped up and treated according to the WebScripting informal protocol.

An important note missing from the description of the protocol is the requirement of the isSelectorExcludedFromWebScript: class method. The default behaviour, for security, is to exclude all methods from access from JavaScript.

Here is a snippet from the TurtleView class. No methods are exclued from JavaScript and the methods right: and up are designed to be called from JavaScript.

- (void)right:(float)a
{
    angle -= a/180*M_PI;
    [self setNeedsDisplay:YES];
}

- (void)up
{
    drawing = NO;
}

...

+ (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector { return NO; }

The method names are translated from Objective-C replacing colons with underscores. This page details the conversion in more detail. Thus a JavaScript script for drawing a flower in TurtleScript is then written as:

function flower(segments, size)
{
    var n, m
    for(n=0; n< segments; n++)
    {
        turtle.right_( 360 / segments )
        for(m=0; m< segments; m++)
        {
            turtle.right_( 360 / segments )
            turtle.forward_( size )
        }
    }
}

// clear and draw a flower
turtle.reset()
flower(26, 7)

That is all there is to it, but to make the interfaces nicer it is possible to use two further aspects of the JavaScript bridge.

It is possible to use the class method webScriptNameForSelector: to create a custom method name for a selector in JavaScript. This allows us to remove all those silly underscores from method names.

+ (NSString *) webScriptNameForSelector:(SEL)sel
{
    if (sel == @selector(forward:))
        return @"forward";
    ...
 
    return nil;
}

Another nice way of accessing your Objective-C object's attributes is to use key-value coding. KVC allows you to build a more natural interface in JavaScript, the attributes are accesed as attributes in JavaScript. As before all attributes are by default excluded from JavaScript for security, and you have to define the isKeyExcludedFromWebScript: class method to specifiy which attibutes are allowed. Also it is possible, just as it was with method names, we can use webScriptNameForKey: to provide further control over how attributes are named and accessed.

A attribute in JavaScript can be accessed very simply. An example would be:

turtle.penSize = 3

One of the annoyances of currently using JavaScript like this is that exceptions are converted to undefined objects when transfered back to Objective-C. A simple solution to this is to trap the exception yourself and return it as a string. Here is how I do it in TurtleScript:

NSString* script = [[NSString alloc] initWithFormat:@"try { %@ } catch (e) { e.toString() }", [scriptTextView string]];

id result = [scriptObject evaluateWebScript:script];
if(![result isMemberOfClass:[WebUndefined class]]) 
    NSLog(@"%@", result);

This will print any exception, or result to standard out, which is very useful for debugging.

The source code and app: TurtleScript.zip (90kB)