Last year, gap intelligence started looking into developing mobile applications for our external customers. While we already have a native iOS app, we selected RubyMotion as the framework for developing these new mobile apps. We're primarily a Ruby shop, so we thought this would minimize the learning curve and streamline development across both iOS and Android.

Our mobile apps need to authenticate users and connect to our existing API. I couldn’t find a complete example of this so I combined different sources to come up with a solution that works for us. Below is the step-by-step process. This assumes you have created a RubyMotion project and have done a tutorial or two to get familiar with the framework. The source code, references, and tutorials are listed at the end. Enjoy.

Setup

First, we'll add some gems to our Gemfile. We'll use afmotion for sending http requests and bubble-wrap to easily parse the JSON responses. After adding, add require 'bubble-wrap' to your Rakefile. Then run bundle and rake pod:install.

gem 'afmotion', '~> 2.6'
gem 'bubble-wrap', '~> 1.8.0'

The first major class we'll look at is our AppDelegate. This is mostly auto-generated by RubyMotion, but we will add the code to initialize and display our SessionsController, which will handle our login.

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)

    @sessions_controller = SessionsController.alloc.init
    @navigation_controller = UINavigationController.alloc.init
    @navigation_controller.pushViewController(@sessions_controller, animated:false)

    @window.rootViewController = @navigation_controller
    @window.makeKeyAndVisible

    true
  end
end

The View Controller

Now we'll start building out our controller, which will inherit from UIViewController. We'll build our login form in the viewDidLoad method (which you've probably seen in RubyMotion tutorials).

class SessionsController < UIViewController
  def viewDidLoad
    super
    self.title = 'Sample Login'
    self.view.backgroundColor = UIColor.whiteColor

    @login_form = LoginFormView.build_login_form
    @login_form.login_button.addTarget(self, action: "login", forControlEvents: UIControlEventTouchUpInside)

    self.view.addSubview @login_form
  end
end

Building the View

You can see that instead of including view logic in the controller, we'll extract this into a LoginFormView.

class LoginFormView < UIView
  attr_accessor :username, :password, :login_button

  def self.build_login_form
    login_form_view = LoginFormView.alloc.initWithFrame(CGRectMake(10, 10, 500, 500))
    login_form_view.build_username
    login_form_view.build_password
    login_form_view.build_login_button
    login_form_view
  end

  def build_username
    @username = UITextField.alloc.initWithFrame([[20, 150], [325, 40]])
    @username.placeholder = 'username'
    @username.setBorderStyle UITextBorderStyleRoundedRect
    addSubview @username
  end

  def build_password
    @password = UITextField.alloc.initWithFrame([[20, 200], [325, 40]])
    @password.placeholder = 'password'
    @password.secureTextEntry = true
    @password.setBorderStyle UITextBorderStyleRoundedRect
    addSubview @password
  end

  def build_login_button
    @login_button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
    @login_button.frame = [[20, 250], [325, 40]]
    @login_button.setTitle("login", forState: UIControlStateNormal)
    addSubview @login_button
  end
end

This class builds the form components, adds them to the view and makes them accessible for the controller. This could probably be refactored or take advantage of a form building gem if it became more complex. Here is what the form looks like:

form screenshot

Talking to the API

Before going back to our controller, we'll build out two other objects that our controller will need. The first is ApiClient. This client will handle communication between our app and the API. This is where we'll use the AFMotion client to make requests. In this example, our API is using username/password authentication through OAuth and will return an access token upon successful login. Realistically, we would probably add methods for any additional endpoints we need to hit on our API after a user is authenticated. For now it's pretty simple:

class ApiClient
  def initialize
    @client = AFMotion::Client.build('https://my-api.com') do
      header "Accept", "application/json"

      response_serializer :json
    end
  end

  def token(username, password, &block)
    @client.post("oauth/token", grant_type: 'password', username: username, password: password) do |result|
      block.call(result.object)
    end
  end
end

Storing User Info

The other object our controller will need is a user model so we can keep track of the user's information and the API token to use in future requests. We'll utilize the app's Persistence store to securely save this data.

class User
  def save_token(username, token)
    App::Persistence['username'] = username
    App::Persistence['token'] = token
  end

  def load_username
    App::Persistence['username']
  end

  def load_token
    App::Persistence['token']
  end

  def reset
    App::Persistence['username'] = nil
    App::Persistence['token'] = nil
  end
end

Connecting All the Pieces

Looking back at our controller, we're now ready to tie it all together. We've added a target to the login_button. The target, also called login, is the name of the method that will be called with the login button is tapped. Let's look at that method along with three helper methods that it will use.

def login
    api_client.token(@login_form.username.text, @login_form.password.text) do |result|
      if result['access_token']
        user.save_token(@login_form.username.text, result['access_token'])
        display_message "Welcome", "Welcome #{@login_form.username.text}."
      else
        display_message "Error", "Invalid Credentials."
      end
    end
  end

  def display_message(title, message)
    alert_box = UIAlertView.alloc.initWithTitle(title, message: message, delegate: nil, cancelButtonTitle: "Ok", otherButtonTitles:nil)
    alert_box.show
  end

  def api_client
    @api_client ||= ApiClient.new
  end

  def user
    @user ||= User.new
  end

The login method will use the api_client to make the request, drawing the username and password from the form view. Once it gets a response it will save the user info and display a welcome message or an error.

form screenshot form screenshot

And that's it. The complete source code can be found here: https://github.com/GapIntelligence/api-auth-sample-ios

Questions and feedback are welcome in the comments or directly at cgoldman@gapintelligence.com. We're always interested in other solutions as we get deeper into working with RubyMotion. Thanks!

RubyMotion logo

References and Tutorials

Ruby on Rails and RubyMotion Authentication: Part One
RubyMotion Keychain Example
Blogtastic RubyMotion App
RubyMotion Tutorial