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 '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.
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).
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
.
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:
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:
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.
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.
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.
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!
References and Tutorials
Ruby on Rails and RubyMotion Authentication: Part One
RubyMotion Keychain Example
Blogtastic RubyMotion App
RubyMotion Tutorial