JavaScriptCore Tutorial for iOS: Getting Started

In this JavaScriptCore tutorial you’ll learn how to build an iOS companion app for a web app, reusing parts of its existing JavaScript via JavaScriptCore. By József Vesza.

Leave a rating/review
Save for later
Share

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:

Movie night is ON…

javascriptcore tutorial

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

… Or Not?

javascriptcore tutorial

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.

Enough theory, let’s get to work!

javascriptcore tutorial

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.
József Vesza

Contributors

József Vesza

Author

Over 300 content creators. Join our team.