I hope you’re not the superstitious sort, as we’ve arrived at part 13 of my “Making Apple TV Apps” series. Its time to connect our JavaScript and native code together so they can work seamlessly to make your app the best it can be.
Building Bridges
As seen in previous parts of this series, our Objective-C and Swift code can access JavaScript methods and properties via the JSContext returned by our AppDelegate’s evaluateAppJavaScriptInContext method. We can also expose Objective-C and Swift functionality and data to our JavaScript code by setting objects in the JSContext object. Let’s review some sample code that demonstrates this.
In the first example, we’re going to provide access to a Swift function from JavaScript, allowing the Swift function to be called from JavaScript as though it is a JavaScript function.
AppDelegate.swift excerpt:
func showSomeSwiftAwesomeness(appController: TVApplicationController) -> @convention(block) (String, String) -> () { return { [weak appController] (title, message) in // Not really very awesome, but you get the point let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) appController?.navigationController.presentViewController(alertController, animated: true, completion: nil) } } func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext) { // Expose our "showSomeSwiftAwesomeness" method to JS let someSwiftAwesomenessClosure = showSomeSwiftAwesomeness(appController) let castedClosure = unsafeBitCast(someSwiftAwesomenessClosure, AnyObject.self) jsContext.setObject(castedClosure, forKeyedSubscript: "showSomeSwiftAwesomeness") }
First, we create a showSomeSwiftAwesomeness Swift function that returns an explicitly typed closure for calling from JavaScript (line 1). The block convention indicates the function type is an Objective-C block type instead of the default Swift function type. This is required due to the connection magic between our native and JavaScript code being performed for us by JavaScript Core, which is an Objective-C based library. It expects things to be done in the Objective-C way, even when we’re coding in Swift.
The closure being returned by the showSomeSwiftAwesomeness method expects two strings and has a void return type. We define the closure to present a view controller as we’ve seen done before (lines 5 and 6), so it is admittedly not that awesome, but it still demonstrates the point. We pass our appController into the closure using a weak reference to avoid unintended cycles (line 3), along with our now named title and message string parameters.
Next up, we implement the evaluateAppJavaScriptInContext method in our AppDelegate (line 10) so we can expose our custom function to JavaScript. We do this by getting a reference to the closure returned by our showSomeSwiftAwesomeness method (line 12) and assigning it to our jsContext object using a key value of the same name (line 14). There is one bit of necessary nastiness here, which is the unsafeBitCast of the returned closure to AnyObject (line 13). We don’t have to resort to this kind of thing very often, but this is one of those times it is needed, as the JSContext setObject method expects an AnyObject instance. Without the forced cast, Xcode will complain that it can’t convert the closure.
With that taken care of, the work needed on the native side of our code is complete. We can now call the added function from JavaScript without any special handling on that side.
application.js excerpt:
App.onLaunch = function(options) { // var alert = createAlert("Hello World!", "Welcome to tvOS"); // navigationDocument.pushDocument(alert); // Use our awesome Swift method instead of TVML to say "Hello World!" showSomeSwiftAwesomeness("Hello World!", "Welcome to tvOS"); }
Here in our test app, we’ve commented out its use of the createAlert helper function and added a call to show some Swift awesomeness in its place.
Before moving on, I want to point out that my example makes this process appear to be more tightly connected than it really is. The name of our native method that returns the Objective-C block closure and the name that is called on the JavaScript side doesn’t have to be the same. For example, you can change the name of our showSomeSwiftAwesomeness native method to getTheClosure. Likewise, the JavaScript method name is entirely dependent on the string we specify for the keyed subscript parameter when we call the JSContext’s setObject method.
Adding Lanes
JavaScript Core also allows us to export multiple methods at once using a protocol implementation. The first step is creating a protocol that defines our data transfer connection points. The protocol has to include the @objc keyword so it will be accessible from Objective-C. It also has to extend the JSExport protocol. The JSExport protocol indicates that the compiler should throw in some data serialization special sauce to manage the conversion of data between the JavaScript and native code sides.
Once we’ve created the protocol, we need to implement it using a class, struct or protocol extension.
Finally, we provide access from JavaScript to our native code in the evaluateAppJavaScriptInContext method as we did before. Lets see this in action.
CloudDataStore.swift excerpt:
@objc protocol CloudDataStoreExport: JSExport { func getItem(key: String) -> AnyObject? func setItem(key: String, _ value: AnyObject) static func create() -> CloudDataStoreExport } @objc class CloudDataStore: NSObject { override init() { super.init() NSNotificationCenter.defaultCenter().addObserver(self, selector: "storeDidChange:", name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification, object: NSUbiquitousKeyValueStore.defaultStore()) NSUbiquitousKeyValueStore.defaultStore().synchronize() } func storeDidChange(notification: NSNotification) { if let ubiquitousKeyValueStore = notification.object as? NSUbiquitousKeyValueStore { ubiquitousKeyValueStore.synchronize() } } } extension CloudDataStore: CloudDataStoreExport { func getItem(key: String) -> AnyObject? { return NSUbiquitousKeyValueStore.defaultStore().objectForKey(key) } func setItem(key: String, _ value: AnyObject) { NSUbiquitousKeyValueStore.defaultStore().setObject(value, forKey: key) } static func create() -> CloudDataStoreExport { return CloudDataStore() } }
The CloudDataStore.swift code is an example taken from a resource I will be sharing with you in the last article of the series. It demonstrates a class that provides access to the iCloud key/value store to JavaScript code.
Lets look at the protocol definition first (lines 1 through 4). It includes the @objc keyword and extends JSExport. In addition to data retrieval and storage methods, it also includes a static create method. This method is needed because we can’t instantiate a native entity directly from JavaScript or vice versa. This is due to the different memory models used in the two platforms.
Next, a default implementation of the protocol is created using a protocol extension (line 28). Don’t worry if you’re not familiar with protocol extensions yet. All you need to know for now is that they are used to provide default implementations of protocol methods. You can replace these default implementations within your own implementation by defining them again in your implementation. Protocol extensions also can only use properties that are defined by the protocol or any protocols that it inherits from. You can’t add new stored properties to a protocol or its implementors from within a protocol extension.
In this example, calls to get or set item data is passed directly on to the appropriate iCloud key-value store methods (lines 30 and 34, respectively). Also, the static create method initializes a new instance of the class and returns it (line 38). There’s no special magic happening here, just some straightforward code.
Turning to our AppDelegate class, we assign the CloudDataStore’s class type to the “CloudDataStore” key instead of a individual method reference like our previous example.
AppDelegate.swift excerpt:
func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext) { jsContext.setObject(CloudDataStore.self, forKeyedSubscript: "CloudDataStore") }
Now we’ll see our native protocol being accessed from the Javascript side in DataController.js (another sample from the book I’ll refer you to at the end of this series).
DataController.js excerpt:
class DataController { constructor(resourceLoader) { this._resourceLoader = resourceLoader; this._cloudDataStore = CloudDataStore.create(); } progressForVideoAtURL(url) { return this._cloudDataStore.getItem(url) || 0; } saveProgressForVideoAtURL(url, progress) { this._cloudDataStore.setItem(url, progress); } }
We call the static create method to get an instance of the class and assign it to a property in the JavaScript DataController class (line 4). In this example, the iCloud key-value store is being used to save and retrieve the viewing progress for video playback (lines 8 and 12). Again as before, calling the native class methods from the JavaScript side looks exactly the same as if the methods had been implemented using JavaScript code.
All Lanes Now Open
That does it for our look at bridging native code with Javascript. When combined with the previous article on extending TVML, you now have the knowledge needed to make the most of both the existing native app approach and the new client/server app approach that Apple introduced with tvOS.
We’re coming around the bend to the final wrap up of this series, but there is still more to explore! I will share additional resources for you to continue your journey with in the next article. I hope you’ll join me for it, or enjoy it when it appears in your inbox if you’ve subscribed.
Leave a Reply