How to use npm packages in native iOS apps

CocoaPods – the de-facto standard in package managers for the Apple ecosystem – currently offers over 36.000 packages. Not all of those are for iOS, some are macOS-only. The Node.js package manager npm offers over half a million packages. That’s a lot! And while not all of those are usable on iOS, a lot of them are. Of course quality is more important than quantity, but the JavaScript world and the packages it offers can be a rich resource for iOS developers to tap into. Before you stop reading: I’m not talking about creating an ugly Frankensteinian web-app-dressed-up-as-a-native app. This article will show you how to use npm packages directly with JavaScriptCore, how to interact with them from Swift and why this can be very powerful if used correctly.

We will build a small sample app that will analyze the sentiment of any English sentence you enter. It does that with the help of the sentiment npm package, some JavaScript, webpack and JavaScriptCore. We will create a native app with native UI – no web views!

This article will show you how to use npm packages directly with JavaScriptCore, how to interact with them from Swift and why this can be very powerful if used correctly.

Prerequisites

You should be comfortable with iOS development, Swift and at least a bit of JavaScript. Make sure you have the latest version of Xcode and an up-to-date version of Node.js installed (I’m using 8.9.1). I use homebrew to install Node.js, but feel free to choose whatever you prefer to install it. OK, ready? Let’s build this app!

Introducing: Sentimentalist

The app we will build will use emojis to display the sentiment of whatever English sentence the user types into a textfield. So naturally “Sentimentalist” is a very fitting name. The actual sentiment analysis will happen in JavaScript, using the “sentiment” npm package. We will build a very minimal JavaScript class that exposes an analyze method. All our JavaScript code (including the npm package) will be bundled up into a single JS file with the help of webpack. In our iOS app we will use the JavaScriptCore framework to load and run that JS file and then we will call the analyze method from Swift and hand its return value from the JavaScript world back to Swift.

You can find the complete source code of the project here. All the different steps are tagged in the git repository, so you can easily check out the app every step of the way.

Let’s get started!

The Xcode Project

Let’s first create the project for our app. Create a new “Single View App” in Xcode, call it “Sentimentalist”, chose “Swift” as the language (or Objective-C if you prefer, it works just as well with JavaScriptCore) and finally make sure that “Include Unit Tests” is checked.

Now we just need to add a basic UI: Open Main.storyboard, add a UITextField to the top of the view and add a UILabel underneath. Add a few constraints so that the textfield is pinned to the top, to the left and to the right and make the label occupy the rest of the space. Choose a nice big font for the label because we want to use it to display emojis; I used 134 points. Connect both the textfield and the label to ViewController as textField and sentimentLabel outlets, respectively. Also connect the textfield’s Editing Changed event to a textDidChange action in the ViewController. You can see what the app should look like by checking out the tag step1 from the repo.

Walking the Line

Next, we will add a new Cocoa Touch class named SentimentAnalyzer. This class will be our gateway to the JavaScript code we want to interact with.

We will treat it as a singleton. That way the JavaScript VM and the JavaScript context that we will interact with only has to be created once and can be re-used throughout the lifetime of the app. A JSVirtualMachine is a “self-contained environment for JavaScript execution” (docs) that is among other things needed to manage the memory of objects that cross the border between the native and the JavaScript world. A JSContext is associated with a JSVirtualMachine and is used to actually evaluate and run JavaScript code.

Everything is being set up in the init method. A snippet of JavaScript is evaluated in the JSContext we’ve created, ready to be called later on. The snippet simply defines an analyze function that will return a random value between -5 and 5. We will later replace that with code that calls the actual sentiment npm package. That package uses a score to represent the sentiment: below zero is negative (the lower the worse) and above 0 is positive (the higher the better).

The analyze method calls the JavaScript function on a background queue and calls the completion handler on the main queue once it has a value to return. As you can see, we don’t have to convert the Swift string in any way in order to use it as an argument for the JavaScript analyze method. Crossing the border between these two worlds is very convenient thanks to JavaScriptCore. A key player here is JSValue: It can represent any JavaScript value, for example numbers, strings, objects, arrays, or functions. A lot of those types can be converted automatically when passed from native to JavaScript. For values that are sent from JavaScript to native, however, a little unwrapping is necessary. That’s why we call toInt32 on the returned JSValue. We know that we are getting an integer back from the analyze function, so we can safely make that conversion. We then create an Int from that value because we don’t want to hand around a specifically sized 32-bit integer in our code.

Finally, we also have a small helper method that returns an emoji for a given sentiment score. The complete class looks like this:

Let’s hook it up in the ViewController:

Check out the tag step2 to see what the app should look like at this point. You can go ahead and run it: Enter some text and watch the emoji change randomly. Congratulations! You are successfully talking to JavaScript from your native iOS app. But wait, there’s more!

The JavaScript App

Now to create our JavaScript application, we will create a new directory called “JS” right inside the “Sentimentalist” directory that Xcode has created for us (the one that contains the .xcodeproj).

Inside the JS directory we will run npm init --yes to create a package.json file with some default settings. Feel free to open it and edit the author, name and description, but it’s not really necessary. The important thing is that this file will contain the names and versions of the npm packages that our app will use. Let’s add those right now. Run npm install sentiment --save to install the sentiment package and to write that dependency to the package.json file. Next run npm install webpack --save-dev to install the webpack tool that we will use to pack up our little JavaScript app into a bundle that we can use inside of our iOS app. We use the --save-dev option here because we only need this package as a development and not as a runtime dependency. If you checkout your project on a different machine, all you have to do is run npm install inside of the “JS” directory to install the dependencies.

Now we are ready to write a tiny JavaScript app that will make use of the sentiment package. Create a index.js file with this content:

Finally we have to configure webpack to build and bundle the app. Webpack is an impressive tool that can analyze which files and packages your project imports and then bundle all of it up into a single file (or – if you prefer – multiple files). Create a new file called webpack.config.js right inside the “JS” directory with this content:

This is a minimal webpack configuration: The entry section can be compared to targets in Xcode. We have one target called Sentimentalist and the file that webpack should use as a starting point to find all the dependencies that need to be included in the final bundle is index.js.

The output section specifies that the bundle should be written to the dist directory, that is should be called Sentimentalist.bundle.js and that the bundle should be accessible as a global variable also named Sentimentalist. This is just the tip of the iceberg of what webpack can do, but it’s enough for our example. Be sure to checkout the documentation so you can master webpack.

To save us from having to type a few extra characters each time we want to build the application, add this line to the ”scripts” section of “package.json”:

"build": "webpack --progress --colors”

Now let’s see if it all works. Run npm run build. You should see an output like this:


Congratulations! It worked!

Check out the tag step3 to see what the app should look like at this point.

Now we just need to integrate the Sentimentalist.bundle.js into our iOS app.

Bringing it all back home

In Xcode, right-click on the Sentimentalist group in the Navigator and select “Add Files to Sentimentalist…”. Navigate to the “JS/dist” directory and select “Sentimentalist.bundle.js”: That will add the file to the project and also make sure that it is copied to the app bundle as part of the build process.

Now all that’s left to do is to use this bundle in the SentimentAnalyzer class. Replace the let jsCode… line with this one:

Finally we need to dig a little deeper to get to our new analyze method in the JavaScript bundle: webpack puts our JS project into a global object called Sentimentalist. Within that our class that contains the static analyze method is called Analyzer. This is what the Swift analyze method needs to look like:

Run the app and try out a few sentences and watch the emoji change!

Check out the tag step4 to see what the app should look like at this point.

Talking back

Using JavaScript in your iOS app is not a one-way street. You can also call native code right from JavaScript. Let’s add the ability to output log messages from JS.

In SwiftAnalyzer’s init method we simply declare a Swift closure with the name nativeLog. We then let the JSContext know about that method by setting it via setObject:forKeyedSubscript:. The fact that we need to add @convention(block) to the closure and cast the Swift string explicitly to NSString are a few inconvenient reminders that JavaScriptCore is not a 100% first class citizen in the Swift world quite yet. This is the new init method:

Let’s switch back to index.js and make use of the native logging function. To play it safe we check if the nativeLog function is defined and then we simply call it:

Run npm run build again and then Build & Run in Xcode: As you type you will see the log messages show up in Xcode’s console!

Checkout the tag step5 in the git repo to see the app at this state.

Is it fast?

Yes. Well, it depends. It is very fast for probably all but the most performance critical operations. In step6 in the git repo you see a performance test being added to the Xcode project. When it is run on my 2017 iPad (not a Pro), it measures an average performance of a little under 4 milliseconds for a call to the analyze method until the callback with the score is called. That’s pretty fast! When targeting a refresh rate of 60 frames per second, you have 16ms per frame before you start dropping frames. That means we could call our analyze method 4 times per frame and still achieve 60fps. Not bad!

So. . . what should I use this for?

JavaScript engines are getting faster and faster. Frameworks like React Native are using the same techniques that we talked about but on a bigger scale. JavaScript on iOS is very fast and very powerful. So if you have existing business logic written in JavaScript it’s worth exploring whether you could just bundle it up and use it in your iOS app instead of having to re-write it.

Code sharing is another area where this approach can shine: It’s entirely possible to write portions of your app in JavaScript and share them between iOS, Android, and Windows. This is especially true when it comes to more or less self-contained pieces of code: I stick something in and get something out. Those units of code are prime candidates for this approach.

And last but not least, there might be npm packages out there that do exactly what you need and you can’t find something comparable on CocoaPods. You might very well be able to easily use it as-is in your iOS app without any noticeable performance implications. JavaScript and iOS are a powerful team!

Johannes has been writing iOS apps since 2008. He co-authored the book "Objective-C Fundamentals" and enjoys brewing his own beer.

Leave a comment

Add your comment here