From Swift to Javascript and Back

I have a confession. I love the web, and the language of the web is JavaScript. There are so many libraries written in JavaScript that I frankly think it’s silly to have to recreate those for other languages. As you might guess, I’m going to talk about how you can integrate JavaScript libraries into your Swift code and then how to can share data and call methods in JavaScript from Swift.

To keep this article reasonable in length, I will briefly cover the basic elements you need and then dive into some examples.

JSVirtualMachine

A JSVirtualMachine is the self-contained environment within which your JavaScript code executes. It has two purposes: 1) to allow you to run code in separate threads concurrently and 2) to allow you to clean up memory you allocate when bridging between Objective C/Swift and JavaScript.

Everything you do in JavaScript ultimately executes in a JSVirtualMachine. When you need to create separate threads, you also need to create a new JSVirtualMachine for each thread. Because the JavaScriptCore Api is thread safe, any attempts to access code running in a JSVirtualMachine from a separate thread will wait until the initial thread is finished. If you had created a background thread for this purpose, you would not get the results you expected.

As for memory issues — the second purpose — the JSVirtualMachine gives you a mechanism to free memory when you are done with an object. You will create a retain cycle anytime you export a Swift/Objective C object to JavaScript and store the value, which is bad news. You also can get a retain cycle when storing JavaScript values in native objects (Swift/Objective C). Retain cycles can occur because a JSValue maintains a strong reference in order to access the underlying JavaScript value on the native side. Similarly, a JSContext maintains a strong reference to any Swift/Objective C object you pass to JavaScript. Therefore, you need to use JSVirtualMachine’s removeManagedReference:withOwner method to clean up memory when you’re done with your objects. In addition, you should use a JSManagedValue object when you need to conditionally store a JSValue on the native side. The JavaScript garbage collector will only clean up objects after you remove references to all managed objects.

JSContext

A JSContext is your JavaScript execution environment, and it is very similar to the window object for a web browser. Everything that you add to this context is accessible by any other object in the same context. This, essentially, is your sandbox for controlling the scope of your JavaScript variables, functions, and objects. You can evaluate scripts written in JavaScript or Objective C/Swift, access values in the JavaScript environment, or send values and objects to JavaScript from Objective C/Swift using the JSContext.

JSValue

A JSValue is the wrapper to an underlying JavaScript value. Its the bridge that allows you to pass or share data between JavaScript and Objective C/Swift. A JSValue has a strong reference to the JSContext to which it belongs. As a word of caution, you need to remember that you can cause a retain cycle if you store the JSValue in Swift/Objective C object. In addition to accessing underlying JavaScript objects, you can use a JSValue to create JavaScript objects that are wrappers to native objects in Swift/Objective C. You can also use them to create JavaScript functions that are written in Objective C/Swift.

JSManagedValue

A JSManagedValue is a JSValue with additional logic to allow a native object to store the underlying JavaScript value without causing a retain cycle. There is built in memory management to automatically release objects when they lose scope, which is similar to how ARC works for Objective C. A JSManagedValue will live on under two conditions: 1) its value is still part of the underlying JavaScript object graph or 2) It’s been added to the JSVirtualMachine using addManagedReference:withOwner method and has not been removed using the removeManagedReference:withOwner method. Otherwise, the JSManagedValue gets set to nil, releasing the JSValue, and is free to be garbage-collected on the JavaScript side.

JSExport

A JSValue can represent and convert all of the JavaScript builtin types to Objective C/Swift and can convert them in the other direction to JavaScript types. However, a JSValue can’t convert Objective C/Swift classes to JavaScript objects without help. The JSExport protocol provides a way to convert Swift/Objective C classes and their underlying instance methods, class functions, and properties into JavaScript objects.

By default when using JSValues, JavascriptCore will convert a Swift/Objective C class into JavaScript object but will not populate instance methods, class functions, or properties. You have to choose which of these you want to expose to JavaScript in your JSExport protocol definition.

JavaScriptCore in Practice

Now that we have described each of the classes and protocols in the JavaScriptCore library, its time to put them to use with some examples.

Setup

Our first step is to create a JSVirtualMachine instance along with a JSContext instance. If you don’t intend to use concurrent operations via threading, you can skip creating a JSVirtualMachine and just create a JSContext with an empty constructor so a JSVirtualMachine will be created for you. Otherwise, you should create and save a JSVirtualMachine instance and pass that as an argument when creating your JSContext instances.

Creating a JSVirtualMachine and JSContext

Calling Functions and Evaluating Scripts

What’s really cool about all of this is that you have the same JavaScript engine that powers WebKit (Safari for iOS and MacOS). Time to ramp this up a bit. I love developing for iOS but I also spend a lot of time developing for Node.js and the web also. I often use JavaScript libraries to cut down on the amount of code I have to write and because there are smarter developers than me out there that have already done some heavy lifting and created some pretty useful stuff. So, our next step is see how we could include and use an existing JavaScript library in our code.

For our example, I am going to use the Moment.js to facilitate a few date time manipulations. By the way, I’m using a Playground for all of my experiments. If you want to follow along, then create a new Playground file with a target of iOS. From there you need the JavaScript code for the Moment.js library, which I installed locally using Bower. Next, I copied the moment dir into my Resources folder for my Playground. If you have trouble with that, you can refer to one of my other posts on using Playgrounds.

What we need to do next is find the path to the moment.js file, copy the contents of the file into a String, and inject it into our JSContext. See below for how you might accomplish that.

Injecting Moment.js into the JSContext

The important take-away here is that if you call a JSContext’s evaluateScript() method you are executing code that, in our case, adds the entire moment.js library as a global object for that particular JSContext. Any scripts or JavaScript objects that are added later will be able to use all of moment.js’s functionality.

Well, at least that sounded good. The truth is that it depends on the content of the script you inject into the JSContext. For the Moment.js lib to work, we need to call it’s constructor and format function via another evaluateScript call. Once we do that, we can get a reference to the moment.js library by using our JSContext’s objectForKeyedSubscript method, which will give us the initialized moment.js object. Refer to the sample above for some examples of how you can invoke a method or call constructors with arguments, which — behind the scenes — make the right things happen in JavaScript. It’s all pretty powerful.

Extended Example

Moving right along, I want provide a more in-depth example to get you thinking about the possibilities of actually using this in your own code. We are going to create a small app for showing contacts. Only instead of real contacts, we are going to use the JavaScript library Faker to provide us new contact details each time we execute. Our example will allow us to model the contact object in Swift and create functions in Swift that can be executed in JavaScript. We also need to get back data from the JavaScript side that has the “faked” contact details. Last for good measure, we will use a WKWebView instance to display our contact in a view on our Playground.

The first step is to create a contact object and a JSExports protocol that outlines what we want exposed to JavaScript. After that we need to use the setObject method of our JSContext to make our contact object accessible in JavaScript.

Exposing Swift to JavaScript

Our next step is to create a Swift function that we can execute in JavaScript. We need to use the @convention(block) syntax to convert our Swift closure to a block that will become a JavaScript function with the same parameters and return values. We again call the setObject method of the JSContext to pass our method to JavaScript. However, this time we need to use the unsafeBitCast method to convert the Swift closure into the AnyObject type for JavaScript to process correctly.

Creating Swift function exposed to JavaScript

Our next series of operations is designed to show you how to call and use the contact object and our createContact function from JavaScript. You will create a contact.js script and add it to the Resources folder of the Playground. Our script is going to have one function that creates the fake contact using the Faker.js library. It’s purpose is to call the method to add the new contact to a view in the Playground and return the created contact for further inspection.

Swift class and method exposed to JavaScript

The code above creates a JSExports protocol and class for another object we would like to expose to JavaScript. We also add this new class to our JSContext so that it is accessible via JavaScript. In addition, we create the Swift method to add the new contact object to a view we can see in our Playground.

Next we need to add the Faker.js library to our JavaScript environment. We follow the same pattern we used for adding Moment.js. We also add our contact.js file to our environment, which adds the createFakeContact() method to our global scope. We then get a reference to the createFakeContact() method on the native side and execute it. I realize there is a lot of back and forth in terms of what’s happening in JavaScript versus what’s occurring in Swift but that’s frankly the point. I want you to see that you can pretty easily pass objects back and forth and execute methods from either side.

Injecting the Faker.js libary into JSContext
contact.js

The last few things we need to do is to add the page.html file, which is loaded by the WKWebView in our ContactDrawable class.

page.hml

To get our contact to display, I created a UIView and assigned it to XCPlaygroundPage.currentPage.liveView. Executing the Playground displays a simple list with randomly created “faked” contact details.

Contact Details displayed in Playground’s Live View

Wrapping Up

The Playground displays a view with contact details that change on each run. With a little imagination, you can adapt the example to make native UI objects that you could create and control from JavaScript. Another possibility would be to extend your app with scripts that you download from a server to instantiate Swift classes to perform native tasks.

On a lighter note, I also wrote an article on why creating native apps is probably the best way to go in most of your mobile development endeavors. Read it and weigh in the discussion!

I hope you found this post interesting. If you enjoyed it, recommend and like it!