Tab Bar native-hybrid app with Xcode storyboards and Turbolinks 5

It's the most promising change to Ruby on Rails I've been waiting for: the ability to create hybrid-native mobile apps. Hybrid apps are native Android and iOS app wrappers that tightly integrate with a Rails app, hosted somewhere online. They offer full native navigation, while keeping the core functionality and UI at the Ruby on Rails server-side end. The two gem versions needed - Rails 5 and Turbolinks 5 - are not officially out yet, but at Firmhouse we've been using the release candidates past few months with much excitement.

In this blog post, I would like to show you how you can use Xcode storyboards to build a hybrid-native app with a tab bar interface.

Creating the project in Xcode

The first step is to create your project in Xcode, and adding the Turbolinks 5 dependencies. There's a good README and Getting Started guide on the Turbolinks GitHub project, so I'll be brief about the steps here.

Start Xcode and create a new project. In the project template picker, choose "Single View Application" from iOS > Application. In the next steps of the wizard, name your application and choose a location to generate the project.

Adding the Turbolinks pod

The second step is to add the external library that integrates your native app with the Turbolinks part of your Rails5 app. You do this by adding a Pod to your project.

First, install the cocoapods gem on your system if you haven't already:

gem install cocoapods  

Then go into your Xcode project directory and initialize a Podfile:

pod init  

This command generates the Podfile file with a build target for your project. Inside the target 'YOURAPP' do block, add the following line:

pod 'Turbolinks', :git => 'https://github.com/turbolinks/turbolinks-ios.git'  

Then run the following command to install the required Turbolinks pod into your Xcode project.

pod install  

When the command finishes, re-open your project in Xcode via the .xcworkspace file. Then try and build your app to see if you get a blank view in the Simulator. That means your project including the Turbolinks pod installed successfully.

Adding a Tab Bar Controller to storyboard

Go into the Main.storyboard file in Xcode. There should be an empty view controller already present, from the project creation. Remove this by selecting "View Controller Scene" and pressing backspace. You should now have a blank storyboard.

Now from the bottom right in the Storyboard editor, find the "Tab Bar Controller" element in the list and drag that onto the Storyboard editor canvas. While dragging you'll see the cursor expand into a Tab Bar Controller template layout with one Tab Bar Controller and two associated View Controllers.

Select the "Tab Bar Controller" interface element from the Storyboard editor sidebar to the left. Then to the right in the attributes inspector, check the "Is Initial View Controller" checkbox. Enabling this option, tells iOS to open up the Tab Bar Controller as the initial view when your app starts.

We'll return to the Storyboard editor in a bit. Let's hook up the code that connects our Ruby on Rails app first.

Creating an application-wide ApplicationController

In a bit, we're going to add two tabs to our app: a "Home" screen, and a "News" screen. Both screens will access two URLs in your Rails app: / and /news/. To do this, we'll need to create code in our Xcode project that opens up these URLs with the new Turbolinks integration.

First, go into the AppDelegate.swift file and add a new line above the class definition where you set the URL for your app to use. For example, the first few lines of that file will look like:

import UIKit

let host = "http://localhost:3000"

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

Now, let's create a top-level ApplicationViewController in our Xcode project, that will contain all the code to hook up an iOS app to a Ruby on Rails app via the Turbolinks bridge.

Right click your project directory in Xcode, choose "New file..." and pick "Cocoa Touch Class" from the file template chooser. Press the "Next" button and enter "ApplicationViewController" in the "Class:" field of the next step. Set the "Subclass of:" field to "UINavigationController" and press "Next again". Click "Create" in the final step to create the file in your project.

Following the wizard results in some template code in the file ApplicationViewController.swift. Remove the pre-generated contents of that file, and replace it with the following:

import UIKit  
import WebKit  
import Turbolinks

class ApplicationViewController: UINavigationController {

   var URL: NSURL {
        return NSURL(string: "\(host)/")!
    }
    private let webViewProcessPool = WKProcessPool()

    private var application: UIApplication {
        return UIApplication.sharedApplication()
    }

    private lazy var webViewConfiguration: WKWebViewConfiguration = {
        let configuration = WKWebViewConfiguration()
        configuration.processPool = self.webViewProcessPool
        return configuration
    }()

    private lazy var session: Session = {
        let session = Session(webViewConfiguration: self.webViewConfiguration)
        session.delegate = self
        return session
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        presentVisitableForSession(session, URL: URL)
    }

    func presentVisitableForSession(session: Session, URL: NSURL, action: Action = .Advance) {
        let visitable = VisitableViewController(URL: URL)

        if action == .Advance {
            pushViewController(visitable, animated: true)
        } else if action == .Replace {
            popViewControllerAnimated(false)
            pushViewController(visitable, animated: false)
        }

        session.visit(visitable)
    }

}

extension ApplicationViewController: SessionDelegate {  
    func session(session: Session, didProposeVisitToURL URL: NSURL, withAction action: Action) {
        presentVisitableForSession(session, URL: URL, action: action)
    }

    func session(session: Session, didFailRequestForVisitable visitable: Visitable, withError error: NSError) {
        let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .Alert)
        alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
        presentViewController(alert, animated: true, completion: nil)
    }

    func sessionDidStartRequest(session: Session) {
        application.networkActivityIndicatorVisible = true
    }

    func sessionDidFinishRequest(session: Session) {
        application.networkActivityIndicatorVisible = false
    }
}

This file contains the main logic to access your Ruby on Rails app via the native-hybrid Turbolinks bridge. It doesn't do anything yet because we haven't hooked up the Storyboard to this code. Continue reading and we'll do just that.

Adding two tabs in the Storyboard

Open Main.storyboard and remove the "Item 1 Scene" and "Item 2 Scene" via the left navigation bar. We're going to replace these with two UINavigationController items in the following steps.

Now find the "Navigation Controller" element from the bottom-right of the storyboard editor and drag it onto your Storyboard editor canvas. This action will also create a scene called "Root View Controller Scene". We don't need that. Select it from the sidebar and press Backspace.

In your Storyboard, you should now have one "Tab Bar Controller Scene" with items and a "Navigation Controller Scene" with items. Let's add the first tab to the Tab Bar Controller and link it to the Navigation Controller that we just created.

Hold control and click the "Tab Bar Controller" UI element in the "Tab Bar Controller" scene in the left sidebar, and drag so a blue line appears and drop it on the "Navigation Controller" UI element. A darkish dialog will appear, in it, click "view controllers" under the "Relationship Segue" heading. Applying the "view controllers" relationship hooks up the Navigation Controller as the first tab in the Tab Bar Controller.

After doing that, your Storyboard should look like the following screenshot. A line should now connect the Tab Bar Controller and Navigation Controller wireframes in the storyboard canvas.

It's now wise to rename the Navigation Controller UI element "Item" in the left sidebar to something sane like "Home" so we can keep our tabs apart. Select it and press Enter, to rename the Navigation Controller UI element.

Let's add the second tab to the Tab Bar Controller.

Do this by selecting the "Home Scene" and copy-pasting it. You'll get a duplicate "Home Scene". Rename the Navigation Controller UI element in the new scene to "News".

Then, to hook it up as a second tab in the Tab Bar Controller: once more, click and drag from the Tab Bar Controller UI element to the new "News" Navigation Controller and select "view controllers" under the "Relationship Segue" heading.

After this step, your Storyboard should look like the following screenshot.

Hooking up the app URLs to the tabs

If you build and run the app in the simulator, you will now see a Tab Bar Controller featuring two tabs with nothing in it. The contents are just black.

The reason for the blank content is that we're not making use yet of the Turbolinks native bridge to hook up those tabs to our app URLs / and /news. Let's do that.

We're going to create two new .swift classes. One for each of the tabs. They will both subclass from the ApplicationViewController class we created earlier in this blog post.

First, create a new file in your project directory. Right click your project directory, select "New file..." and choose "Cocoa Touch Class". For the "Class: " field enter "HomeViewController" and for "Subclass of:" choose "ApplicationViewController".

Open the new file HomeViewController.swift and replace it with the following:

import UIKit

class HomeViewController: ApplicationViewController {

    override var URL: NSURL {
        return NSURL(string: "\(host)/")!
    }

}

Now create a new file NewsViewController.swift just like how you created the HomeViewController. Replace that file's content with:

import UIKit

class NewsViewController: ApplicationViewController {

    override var URL: NSURL {
        return NSURL(string: "\(host)/news")!
    }

}

We've created two new controllers and set the custom path to open for each of them. HomeViewController will open / and NewsViewController will open /news.

Let's tell our Storyboard to open these controllers when running the app.

Hooking up the Storyboard to our View Controllers

Open up Main.storyboard and select the "Home" Navigation Controller inside the "Home Scene". In the info sidebar to the right, select the "Show Identity inspector" item (small newspaper).

From the Class dropdown, select the "HomeViewController".

Do the same for the "News" Navigation controller. Select it, in your Storyboard sidebar and from the Identity info-pane, set Class to "NewsViewController."

Running your app

Phew! That should be it. Just to summarize, you have now:

  • Created an Xcode iOS app project
  • Added the Turbolinks Pod
  • Created a Storyboard with a Tab Bar Controller
  • Added the code that adds the Turbolinks bridge to your project via an ApplicationViewController
  • Added two UINavigationController's in your Storyboard and hooked them up to the Tab Bar Controller as tabs.
  • Created HomeViewController and NewsViewController classes where you defined the URL to use.
  • Linked the HomeViewController and NewsViewController up to the respective tabs in the Storyboard.

Depending on which top level host URL you chose in AppDelegate.swift, you should now have the app running with two tabs. Each of the tabs linking to their navigation stack of your Rails application.

Questions or comments?

Did you enjoy this post? If you have any questions or comments, please let me know! You can reach me on Twitter via @michiels or send me an email at michiel@firmhouse.com.