JavaScriptCore Tutorial for iOS: Getting Started

József Vesza

JavaScriptCore-feature

Note: Updated for Xcode 8, iOS 10, and Swift 3 on 24-09-2016

Since the introduction of Swift in 2014, its popularity has skyrocketed: according to the TIOBE index from February 2016 it’s already listed in 16th place. But only a few spaces ahead at number 9, you’ll find a language that seems quite the opposite of Swift: JavaScript. Swift puts a lot of effort on compile-time safety, while JavaScript is weakly typed and dynamic.

Swift and JavaScript may look different, but there is one important thing that binds them together: you can use them together to make a slick iOS app!

In this JavaScriptCore tutorial you’ll build an iOS companion app for a web page, reusing parts of its existing JavaScript code. In particular, you’ll learn about:

  • The components of the JavaScriptCore framework.
  • How to invoke JavaScript methods from your iOS code.
  • How to access your native code from JavaScript.

Note: You don’t need to be experienced in JavaScript to follow along. If this JavaScriptCore tutorial has piqued your interest in learning the language, Mozilla Developer Network is an excellent resource for beginners — or you can also choose to skip straight to the good parts. :]

Getting Started

Download the starter project for this tutorial and unzip it. You’ll be greeted by the following folder structure:

  • Web: Contains the HTML and CSS for the web app that you’ll be converting to iOS.
  • Native: The iOS project. This is where you’ll make all the changes in this tutorial.
  • js: Contains the JavaScript code used in the project.

The app is named Showtime; you can use it to search for movies on iTunes by price. To see it in action, open Web/index.html in your favorite browser, enter your preferred price, and hit Return:

javascriptcore tutorial

Movie night is ON…

To test Showtime on iOS, open the Xcode project residing in Native/Showtime. Build and run the app to take a look:

javascriptcore tutorial

… Or Not?

As you can see, the mobile companion isn’t quite feature-ready, but you’ll fix it shortly. The project already contains some code; feel free to browse through it to get a better idea of what’s going on. The app aims to provide a similar experience to the web page: it will display the search results in a collection view.

What is JavaScriptCore?

The JavaScriptCore framework provides access to WebKit’s JavaScript engine. Originally, the framework had a Mac-only, C API, but iOS 7 and OS X 10.9 shipped with a much nicer Objective-C wrapper. The framework enables powerful interoperability between your Swift/Objective-C and JavaScript code.

Note: React Native is an impressive demonstration of the power of JavaScriptCore. If you’re curious about building native apps with JavaScript, make sure you check out our Introducing React Native tutorial on this site.

In this section, you’ll take a closer look at the API. Under the hood, JavaScriptCore consists of a couple of key components: JSVirtualMachine, JSContext, and JSValue. Here’s how they all fit together.

JSVirtualMachine

JavaScript code is executed in a virtual machine represented by the JSVirtualMachine class. You won’t normally have to interact with this class directly, but there is one main use case for it: supporting concurrent JavaScript execution. Within a single JSVirtualMachine, it’s not possible to execute multiple threads at the same time. In order to support parallelism, you must use multiple virtual machines.

Each instance of JSVirtualMachine has its own heap and its own garbage collector, which means that you can’t pass objects between virtual machines. A virtual machine’s garbage collector wouldn’t know how to deal with a value from a different heap.

JSContext

A JSContext object represents an execution environment for JavaScript code. It corresponds to a single global object; its web development equivalent would be a window object. Unlike with virtual machines, you are free to pass objects between contexts (given that they reside in the same virtual machine).

JSValue

JSValue is the primary data type you’ll have to work with: it can represent any possible JavaScript value. An instance of JSValue is tied to the JSContext object it lives in. Any value that comes from the context object will be of JSValue type.

This diagram shows how each piece of the puzzle works together:

javascriptcore tutorial

Now that you have a better understanding about the possible types in the JavaScriptCore framework, it’s finally time to write some code.

javascriptcore tutorial

Enough theory, let’s get to work!

Invoking JavaScript Methods

Back in Xcode, expand the Data group in the project navigator and open MovieService.swift. This class will retrieve and process movie results from iTunes. Right now, it’s mostly empty; it will be your job to provide the implementation for the method stubs.

The general workflow of MovieService will look like the following:

  • loadMoviesWith(limit:onComplete:) will fetch the movies.
  • parse(response:withLimit:) will reach out to the shared JavaScript code to process the API response.

The first step is to fetch the list of movies. If you’re familiar with JavaScript development, you’ll know that networking calls typically use XMLHttpRequest objects. This object isn’t part of the language itself, however, so you can’t use it in the context of an iOS app. Instead, you’ll have to resort to native networking code.

Within the MovieService class, find the stub for loadMoviesWith(limit:onComplete:) and modify it to match the code below:

func loadMoviesWith(limit: Double, onComplete complete: @escaping ([Movie]) -> ()) {
  guard let url = URL(string: movieUrl) else {
    print("Invalid url format: \(movieUrl)")
    return
  }
  
  URLSession.shared.dataTask(with: url) { data, _, _ in
    guard let data = data, let jsonString = String(data: data, encoding: String.Encoding.utf8) else {
      print("Error while parsing the response data.")
      return
    }
    
    let movies = self.parse(response: jsonString, withLimit: limit)
    complete(movies)
  }.resume()
}

The snippet above uses the default shared URLSession session to fetch the movies. Before you can pass the response to the JavaScript code, you’ll need to provide an execution context for the response. First, import JavaScriptCore by adding the following line of code to the top of MovieService.swift, below the existing UIKit import:

import JavaScriptCore

Then, define the following property in MovieService:

lazy var context: JSContext? = {
  let context = JSContext()
  
  // 1
  guard let
    commonJSPath = Bundle.main.path(forResource: "common", ofType: "js") else {
      print("Unable to read resource files.")
      return nil
  }
  
  // 2
  do {
    let common = try String(contentsOfFile: commonJSPath, encoding: String.Encoding.utf8)
    _ = context?.evaluateScript(common)
  } catch (let error) {
    print("Error while processing script file: \(error)")
  }
  
  return context
}()

This defines context as a lazy JSContext property:

  1. First, you load the common.js file from the application bundle, which contains the JavaScript code you want to access.
  2. After loading the file, the context object will evaluate its contents by calling context.evaluateScript(), passing in the file contents for the parameter.

Now it’s time to invoke the JavaScript methods. Still in MovieService.swift, find the method stub for parse(response:withLimit:), and add the following code:

func parse(response: String, withLimit limit: Double) -> [Movie] {
  // 1
  guard let context = context else {
    print("JSContext not found.")
    return []
  }
  
  // 2
  let parseFunction = context.objectForKeyedSubscript("parseJson")
  guard let parsed = parseFunction?.call(withArguments: [response]).toArray() else {
    print("Unable to parse JSON")
    return []
  }
  
  // 3
  let filterFunction = context.objectForKeyedSubscript("filterByLimit")
  let filtered = filterFunction?.call(withArguments: [parsed, limit]).toArray()

  // 4
  return []
}

Taking a look at the process, step by step:

  1. First, you make sure the context object is properly initialized. If there were any errors during the setup (e.g.: common.js was not in the bundle), there’s no point in resuming.
  2. You ask the context object to provide the parseJSON() method. As mentioned previously, the result of the query will be wrapped in a JSValue object. Next, you invoke the method using call(withArguments:), where you specify the arguments in an array format. Finally, you convert the JavaScript value to an array.
  3. filterByLimit() returns the list of movies that fit the given price limit.
  4. So you’ve got the list of movies, but there’s still one missing piece: filtered holds a JSValue array, and you need to map them to the native Movie type.
Note: You might find the use of objectForKeyedSubscript() a little odd here. Unfortunately, Swift only has access to these raw subscripting methods rather than having them translated into a proper subscript method. Objective-C can use subscripting syntax with square brackets, however.

Exposing Native Code

One way to run native code in the JavaScript runtime is to define blocks; they’ll be bridged automatically to JavaScript methods. There is, however, one tiny issue: this approach only works with Objective-C blocks, not Swift closures. In order to export a closure, you’ll have to perform two tasks:

  • Annotate the closure with the @convention(block) attribute to bridge it to an Objective-C block.
  • Before you can map the block to a JavaScript method call, you’ll need to cast it to an AnyObject.

Switch over to Movie.swift and add the following method to the class:

static let movieBuilder: @convention(block) ([[String : String]]) -> [Movie] = { object in
  return object.map { dict in
    
    guard
      let title = dict["title"],
      let price = dict["price"],
      let imageUrl = dict["imageUrl"] else {
        print("unable to parse Movie objects.")
        fatalError()
    }
    
    return Movie(title: title, price: price, imageUrl: imageUrl)
  }
}

This closure takes an array of JavaScript objects (represented as dictionaries) and uses them to construct Movie instances.

Switch back to MovieService.swift. In parse(response:withLimit:), replace the return statement with the following code:

// 1
let builderBlock = unsafeBitCast(Movie.movieBuilder, to: AnyObject.self)

// 2
context.setObject(builderBlock, forKeyedSubscript: "movieBuilder" as (NSCopying & NSObjectProtocol)!)
let builder = context.evaluateScript("movieBuilder")

// 3
guard let unwrappedFiltered = filtered,
  let movies = builder?.call(withArguments: [unwrappedFiltered]).toArray() as? [Movie] else {
    print("Error while processing movies.")
    return []
}

return movies
  1. You use Swift’s unsafeBitCast(_:to:) function to cast the block to AnyObject.
  2. Calling setObject(_:forKeyedSubscript:) on the context lets you load the block into the JavaScript runtime. You then use evaluateScript() to get a reference to your block in JavaScript.
  3. The final step is to call your block from JavaScript using call(withArguments:), passing in the array of JSValue objects as the argument. The return value can be cast to an array of Movie objects.

It’s finally time to see your code in action! Build and run. Enter a price in the search field and you should see some results pop up:

javascriptcore tutorial

That’s more like it!

With only a few lines of code, you have a native app up and running that uses JavaScript to parse and filter results! :]

Using The JSExport Protocol

The other way to use your custom objects in JavaScript is the JSExport protocol. You have to create a protocol that conforms to JSExport and declare the properties and methods, that you want to expose to JavaScript.

For each native class you export, JavaScriptCore will create a prototype within the appropriate JSContext instance. The framework does this on an opt-in basis: by default, no methods or properties of your classes expose themselves to JavaScript. Instead, you must choose what to export. The rules of JSExport are as follows:

  • For exported instance methods, JavaScriptCore creates a corresponding JavaScript function as a property of the prototype object.
  • Properties of your class will be exported as accessor properties on the prototype.
  • For class methods, the framework will create a JavaScript function on the constructor object.

To see how the process works in practice, switch to Movie.swift and define the following new protocol above the existing class declaration:

import JavaScriptCore

@objc protocol MovieJSExports: JSExport {
  var title: String { get set }
  var price: String { get set }
  var imageUrl: String { get set }
  
  static func movieWith(title: String, price: String, imageUrl: String) -> Movie
}

Here, you specify all the properties you want to export and define a class method to construct Movie objects in JavaScript. The latter is necessary since JavaScriptCore doesn’t bridge initializers.

It’s time to modify Movie to conform to JSExport. Replace the entire class with the following:

class Movie: NSObject, MovieJSExports {
  
  dynamic var title: String
  dynamic var price: String
  dynamic var imageUrl: String
  
  init(title: String, price: String, imageUrl: String) {
    self.title = title
    self.price = price
    self.imageUrl = imageUrl
  }
  
  class func movieWith(title: String, price: String, imageUrl: String) -> Movie {
    return Movie(title: title, price: price, imageUrl: imageUrl)
  }
}

The class method will simply invoke the appropriate initializer method.

Now your class is ready to be used in JavaScript. To see how you can translate the current implementation, open additions.js from the Resources group. It already contains the following code:

var mapToNative = function(movies) {
  return movies.map(function (movie) {
    return Movie.movieWithTitlePriceImageUrl(movie.title, movie.price, movie.imageUrl);
  });
};

The above method takes each element from the input array, and uses it to build a Movie instance. The only thing worth pointing out is how the method signature changes: since JavaScript doesn’t have named parameters, it appends the extra parameters to the method name using camel case.

Open MovieService.swift and replace the closure of the lazy context property with the following:

lazy var context: JSContext? = {

  let context = JSContext()
  
  guard let
    commonJSPath = Bundle.main.path(forResource: "common", ofType: "js"),
    let additionsJSPath = Bundle.main.path(forResource: "additions", ofType: "js") else {
      print("Unable to read resource files.")
      return nil
  }
  
  do {
    let common = try String(contentsOfFile: commonJSPath, encoding: String.Encoding.utf8)
    let additions = try String(contentsOfFile: additionsJSPath, encoding: String.Encoding.utf8)
    
    context?.setObject(Movie.self, forKeyedSubscript: "Movie" as (NSCopying & NSObjectProtocol)!)
    _ = context?.evaluateScript(common)
    _ = context?.evaluateScript(additions)
  } catch (let error) {
    print("Error while processing script file: \(error)")
  }
  
  return context
}()

No big changes here. You load the contents of additions.js into your context. By using setObject(_:forKeyedSubscript:) on JSContext, you also make the Movie prototype available within the context.

There is only one thing left to do: in MovieService.swift, replace the current implementation of parse(response:withLimit:) with the following code:

func parse(response: String, withLimit limit: Double) -> [Movie] {
  guard let context = context else {
    print("JSContext not found.")
    return []
  }
  
  let parseFunction = context.objectForKeyedSubscript("parseJson")
  guard let parsed = parseFunction?.call(withArguments: [response]).toArray() else {
    print("Unable to parse JSON")
    return []
  }
  
  let filterFunction = context.objectForKeyedSubscript("filterByLimit")
  let filtered = filterFunction?.call(withArguments: [parsed, limit]).toArray()
  
  let mapFunction = context.objectForKeyedSubscript("mapToNative")
  guard let unwrappedFiltered = filtered,
    let movies = mapFunction?.call(withArguments: [unwrappedFiltered]).toArray() as? [Movie] else {
    return []
  }
  
  return movies
}

Instead of the builder closure, the code now uses mapToNative() from the JavaScript runtime to create the Movie array. If you build and run now, you should see that the app still works as it should:

javascriptcore tutorial

Congratulations! Not only have you created an awesome app for browsing movies, you have done so by reusing existing code — written in a completely different language!

javascriptcore tutorial

Now that’s what I call seamless user experience!

Where to Go From Here?

You can download the completed project for this tutorial here.

If you wish to learn more about JavaScriptCore, check out Session 615 from WWDC 2013.

I hope you enjoyed this JavaScriptCore tutorial. If you have any questions or comments, please join the forum discussion below!

József Vesza

Software developer on multiple platforms, mainly mobile.
Interface Builder and Auto Layout advocate. I have been working with iOS since 2012.
Get in touch via Twitter or LinkedIn.
Browse my code on GitHub and Stack Overflow.

Other Items of Interest

Big Book SaleAll raywenderlich.com iOS 11 books on sale for a limited time!

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

iOS Team

... 71 total!

Android Team

... 15 total!

Unity Team

... 11 total!

Articles Team

... 15 total!

Resident Authors Team

... 17 total!

Podcast Team

... 7 total!

Recruitment Team

... 9 total!