Building an iOS app with Turbolinks-iOS

Over the last couple of months, we’ve been shipping quite some hybrid iOS and Android apps.

Initially, we were building these apps with RubyMotion, which is excellent for building native apps. But recently, we’ve decided to give Turbolinks 5 and the accompanied iOS and Android adapters a spin.

In this post, I’ll briefly go over some of the things I ran into when wrapping our Rails app into a native iOS app with the turbolinks-ios adapter.

Preparing your Rails app

For the adapter to work, you need to have Turbolinks 5 set up in your Rails app. Rails 5 will include Turbolinks 5 by default. So, for now, you can either choose to upgrade to the latest Rails 5 beta or install the Turbolinks gem manually.

Form submission

When using Turbolinks, form submissions have to be done via XHR. In your view, this is as easy as adding the remote: true attribute to your form_for tag. However, the response to a form submission does need some more manual changes.

As an example, I’ll take this typical pattern in Rails for saving a model:

def create  
  @post = Post.new(post_params)

  if @post.save
    redirect_to @post
  else
    render :new
  end
end  

In this case, when the @post is valid, Turbolinks will detect the redirect and just forward the client to the @post. However, when the @post is not valid, we want to execute some Javascript to update the view and let the user know something went wrong saving the @post.

For this, I’ve added a SJR response that will replace the current form on the page with the updated form:

# app/views/posts/new.js.erb
$(“.post-form”).html(“<%= j render ‘form’ %>”)

Now, when the @post cannot get saved, the view will get updated with the validation errors. Assuming you’re rendering those in the form partial of course.

Bridge between JavaScript and Swift

One of the main advantages of building a Hybrid app is having access to the native APIs. So you'll be able to use things like native interface components, setting up push notifications or leveraging the device's hardware.

By default, the turbolinks-ios adapter uses an implementation of the UIViewController to navigate through web pages. Basically by initiating a new ViewController on each request and adding it to the stack of the NavigationController, which results in a native-like user experience when traversing between screens:

img

This default iOS navigation bar didn’t match the design of our app, so we decided to hide this navigation bar and let our Rails app render a custom back button. However, we do want this button to behave similarly to the native back button: deallocate the ViewController and slide-animate back to the previous view.

Our desired situation: click link to open new viewcontroller, click back-icon to slide back:
img

To accomplish this, we need a way for our Rails app’s JavaScript to execute a native (Swift) method. To set this up, we’re using WKScriptMessageHandler to extend our UINavigationController.

Adding the messageHandler to your wrapper

I’ve added the following code to my ApplicationController.swift:

    private lazy var webViewConfiguration: WKWebViewConfiguration = {
        let configuration = WKWebViewConfiguration()
        configuration.userContentController.addScriptMessageHandler(self, name: "closeViewController")
        return configuration
    }()

extension ApplicationController: WKScriptMessageHandler {  
    func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
        popViewControllerAnimated(true)
    }
}

This code will register the userContentController as Handler for any message the WebView receives with the name closeViewController. The userContentController method will call popViewControllerAnimated(true) which will pop the currently visible view controller.

Calling the method from your Rails app

After adding this message handler, we’re adding a back button to our iOS app with data-behavior=“close” attribute and the following iOS specific javascript:

$(document).on "click", "[data-behavior=close]", (e) ->
    webkit.messageHandlers.closeViewController.postMessage('close')

Note that this button should be either a link- or button tag otherwise the WebView won’t register the taps/clicks.

Questions or comments?

So far, I like the Turbolinks adapters a lot! It provides an excellent starting point for building hybrid apps.

We’re still actively developing with the Turbolinks adapters, so we are going to run into many other challenges from here, but at least we’ll have more stuff to blog about!

If you have any questions or comments, feel free to reach out to me on Twitter: @joshuajansen or email: joshua@firmhouse.com