神刀安全网

JavaScriptCore Tutorial for iOS: Getting Started

JavaScriptCore Tutorial for iOS: Getting Started

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 for iOS: Getting Started

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 for iOS: Getting Started

… 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 for iOS: Getting Started

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 for iOS: Getting Started

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:

  • loadMoviesWithLimit(_:onComplete:) will fetch the movies.
  • parseResponse(_: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 loadMoviesWithLimit(_:onComplete:) and modify it to match the code below:

func loadMoviesWithLimit(limit: Double, onComplete complete: [Movie] -> ()) {   guard let url = NSURL(string: movieUrl) else {     print("Invalid url format: /(movieUrl)")     return   }     NSURLSession.sharedSession().dataTaskWithURL(url) { data, _, _ in     guard let data = data,         jsonString = String(data: data, encoding: NSUTF8StringEncoding) else {       print("Error while parsing the response data.")       return     }       let movies = self.parseResponse(jsonString, withLimit:limit)     complete(movies)     }.resume() }

The snippet above uses the default shared NSURLSession 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 = NSBundle.mainBundle().pathForResource("common", ofType: "js") else {     print("Unable to read resource files.")     return nil   }     // 2   do {     let common = try String(contentsOfFile: commonJSPath, encoding: NSUTF8StringEncoding)     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 parseResponse(_:withLimit:) , and add the following code:

func parseResponse(response: String, withLimit limit: Double) -> [Movie] {   // 1   guard let context = context else {     print("JSContext not found.")     return []   }     // 2   let parseFunction = context.objectForKeyedSubscript("parseJson")   let parsed = parseFunction.callWithArguments([response]).toArray()     // 3   let filterFunction = context.objectForKeyedSubscript("filterByLimit")   let filtered = filterFunction.callWithArguments([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 callWithArguments(_:) , 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"],         price = dict["price"],         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 parseResponse(_:withLimit:) , replace the return statement with the following code:

// 1 let builderBlock = unsafeBitCast(Movie.movieBuilder, AnyObject.self)   // 2 context.setObject(builderBlock, forKeyedSubscript: "movieBuilder") let builder = context.evaluateScript("movieBuilder")   // 3 guard let unwrappedFiltered = filtered,   let movies = builder.callWithArguments([unwrappedFiltered]).toArray() as? [Movie] else {   print("Error while processing movies.")   return [] }   return movies

  1. You use Swift’s unsafeBitCast(_:_:) 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 callWithArguments(_:) , 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 for iOS: Getting Started

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 movieWithTitle(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 movieWithTitle(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 = NSBundle.mainBundle().pathForResource("common", ofType: "js"),       additionsJSPath = NSBundle.mainBundle().pathForResource("additions", ofType: "js") else {     print("Unable to read resource files.")     return nil   }     do {     let common = try String(contentsOfFile: commonJSPath, encoding: NSUTF8StringEncoding)     let additions = try String(contentsOfFile: additionsJSPath, encoding: NSUTF8StringEncoding)       context.setObject(Movie.self, forKeyedSubscript: "Movie")     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 parseResponse(_:withLimit:) with the following code:

func parseResponse(response: String, withLimit limit: Double) -> [Movie] {   guard let context = context else {     print("JSContext not found.")     return []   }     let parseFunction = context.objectForKeyedSubscript("parseJson")   let parsed = parseFunction.callWithArguments([response]).toArray()     let filterFunction = context.objectForKeyedSubscript("filterByLimit")   let filtered = filterFunction.callWithArguments([parsed, limit]).toArray()     let mapFunction = context.objectForKeyedSubscript("mapToNative")   guard let unwrappedFiltered = filtered,     movies = mapFunction.callWithArguments([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 for iOS: Getting Started

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 for iOS: Getting Started

Now that’s what I call seamless user experience!

Where to Go From Here?

You can download the completed project for this tutorialhere.

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!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » JavaScriptCore Tutorial for iOS: Getting Started

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮