Lessons Building a Simple iOS + Backbone.js + Ruby on Rails App

03.10.2015

Recently, I spent some time learning how to tie together a Rails back-end, an iOS client, and a Backbone.js web interface. For future reference, I am writing this post of my notes during the development process.

Authentication

The first challenge was building an authentication system that would work for both a web and mobile client. Traditional Rails apps use a session based authentication system where users have their encrypted session id and user id passed along with every request. With Devise, you could have this mechanism all set up for you within minutes. Adding an iOS client requires a bit more thought and work. After some research, I ended up going with a JSON authentication solution by using the simple_token_authentication gem in conjunction with Devise. Every user that logs in gets an “authentication_token”, which is saved as a field on the User model. When a user logs in with correct credentials, they will receive the authentication token, which can be saved in memory on the iOS client. You can pass this token along with every request to authenticate the user.

Gemfile

gem 'simple_token_authentication'

user.rb

class User < ActiveRecord::Base
  acts_as_token_authenticatable
end

sessions_controller.rb

module Api
  module V1
    class SessionsController < Devise::SessionsController
      skip_before_action :verify_authenticity_token
      def create
        self.resource = warden.authenticate!(auth_options)
        sign_in(resource_name, resource)
        current_user.update authentication_token: nil
        respond_to do |format|
          format.json {
            render :json => {
              :user => current_user,
              :status => :ok,
              :authentication_token => current_user.authentication_token
            }
          }
        end
      end
    end
  end
end

LoginViewController.m

- (IBAction)signIn:(id)sender {
    NSString *requestString = @"http://localhost:3000/api/v1/sessions/create.json";
    NSURL *url = [NSURL URLWithString:requestString];
    NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
    [req setHTTPMethod:@"POST"];
    //NSDictionary *userDict = @{@"user": @{@"email": @"test@example.com", @"password": @"password"}};
    NSDictionary *userDict = @{@"user": @{@"email": self.username.text, @"password": self.password.text}};
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:userDict options:NSJSONWritingPrettyPrinted error:nil];
    [req setHTTPBody: jsonData];
    [req addValue:@"application/json" forHTTPHeaderField:@"Accept"];
    [req addValue:@"application/json" forHTTPHeaderField:@"Content-type"];
    NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (!error) {
            NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            NSString *auth_token = [jsonObject objectForKey:@"authentication_token"];
            NSString *user_email = [[jsonObject objectForKey:@"user"] objectForKey:@"email"];
            NSDictionary *authInfo = @{@"email": user_email, @"authentication_token": auth_token};
            dispatch_async(dispatch_get_main_queue(), ^{
                AppDelegate *app = [UIApplication sharedApplication].delegate;
                app.authInfo = authInfo;
                app.session = self.session;
                [app setRoots];
            });
        } else {
            UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:@"There was an error." preferredStyle:UIAlertControllerStyleAlert];
            UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
            }];
            [alert addAction:defaultAction];
            [self presentViewController:alert animated:YES completion:nil];
        }
    }];
    [dataTask resume];
}

Backbone.js pushState

How do you handle anchor tags in a Backbone.js app? One way to deal with it is to use a “catch all” listener for any clicks on anchor tags. We override the default behavior and instead use Backbone’s routing system.

index.html.erb

$(function() {
  window.router = new PhotoCritic.Routers.PhotosRouter();
  Backbone.history.start({pushState: true});
    $(document).on('click', 'a:not([data-bypass])', function (evt) {
        var href = $(this).attr('href');
        var protocol = this.protocol + '//';
        if (href.slice(protocol.length) !== protocol) {
          evt.preventDefault();
          window.router.navigate(href, true);
        }
      });
});

nav.hbs

<div id="photo-critic-nav">
  <ul class="nav nav-tabs">
    <li><a href="/" id="home-link">Home</a></li>
    <li><a href="/photos/new" id="new-link">New Photo</a></li>
    <li><a href="/photos" id="photos-link">My Photos</a></li>
  </ul>
</div>

iOS Frame and Coordinate System

Understanding iOS’s frames and bounds was another hurdle that mostly came about as I was trying to position an Activity Indicator in the middle of the screen. A frame is made up of a CGRect struct, which has an x and y origin and width and height. Every view has a frame. The x and y origin is the top left point of of the view relative to the superview’s top left origin (0, 0). Center is the center of the view relative to the superview’s coordinate system.

iOS Protocols and Delegates

A protocol is essentially a set of “rules” that a class must follow. A class must implement certain methods in order to “conform” to a protocol. For instance the UIImagePickerControllerDelegate protocol must implement the - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info method to handle what happens when a user picks an image. The protocol must be declared in the interface of the controller.

NewPhotoViewController.m

@interface NewPhotoViewController () <UIImagePickerControllerDelegate>
@end

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
    UIImage *image = info[UIImagePickerControllerOriginalImage];
    self.imageView.image = image;
    self.submitButton.hidden = NO;
    [self dismissViewControllerAnimated:YES completion:nil];
}

iOS ViewDidLoad vs. ViewDidLayoutSubviews

It is important to know that auto-layout gets applied after ViewDidLoad. If you are trying to edit the layout in code, you have to use ViewDidLayoutSubviews, otherwise the auto-layout from the interface builder will override your changes and you’ll be left wondering why your code layout changes aren’t working.

The App Delegate

The app delegate can be accessed anywhere from the application using AppDelegate *app = [UIApplication sharedApplication].delegate;.
This is useful when you need to change something stored at the root level in the view hierarchy or call a method that is defined in the app delegate implementation. In this app I use it to set the root view controller after the user logs in.

dispatch_async

If you try to call some methods on the main thread within an asynchronous process callback, you’ll likely get some unexpected results. You might need to call the dispatch_async method with the code you want to execute passed in as a block. For example, this is used to set the session information and new root view controller after a log in request and response is made.

LoginViewController.m

NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (!error) {
            NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            NSString *auth_token = [jsonObject objectForKey:@"authentication_token"];
            NSString *user_email = [[jsonObject objectForKey:@"user"] objectForKey:@"email"];
            NSDictionary *authInfo = @{@"email": user_email, @"authentication_token": auth_token};
            dispatch_async(dispatch_get_main_queue(), ^{
                AppDelegate *app = [UIApplication sharedApplication].delegate;
                app.authInfo = authInfo;
                app.session = self.session;
                [app setRoots];
            });
        } else {
            UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:@"There was an error." preferredStyle:UIAlertControllerStyleAlert];
            UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 
            }];
            [alert addAction:defaultAction];
            [self presentViewController:alert animated:YES completion:nil];
        }
    }];

Backbone PubSub

Publish Subscribe (PubSub) is a useful design pattern for sending notifications to different parts of the application. I needed this to implement the infinite scroll with Backbone.

class Backbone.PubSub extends Backbone.Events
Backbone.PubSub.on('loadMore', @loadMore)
Backbone.PubSub.trigger('loadMore')
Backbone.PubSub.off('loadMore')

Ajax File Uploads

To upload files with Ajax, you have to use the FormData object.

formData = new FormData()
formData.append('photo[pic]', input[0].files[0])
formData.append('photo[title]', title)
@model.data = formData
@model.unset("errors")
@model.validate()
$.ajax({
  url: '/api/v1/photos',
  data: formData,
  cache: false,
  contentType: false,
  processData: false,
  type: 'POST',
  success: (data) =>
    Backbone.history.navigate("/photos/#{data.photo.id}", {trigger: true})
  error: (model, xhr, options) ->
    alert('Error')
})

iOS Pointers

Certain variable declarations marked with an asterisk represents a pointer to an object. Those without asterisks are not pointers, but represent a C structure. Things like CGRect, Int, etc. It’s important to know how pointers work in Objective-C to understand memory management and Automatic Reference Counting (ARC), and to debug strong reference cycles should they happen to occur.

GitHub

https://github.com/travisluong/photo-critic
https://github.com/travisluong/photo-critic-client

Sources

http://jessewolgamott.com/blog/2012/01/19/the-one-with-a-json-api-login-using-devise/

http://provoost.tumblr.com/post/80873086965/json-api-authentication-using-devise-tokens

https://gist.github.com/josevalim/fb706b1e933ef01e4fb6

http://stackoverflow.com/questions/5082738/ios-calling-app-delegate-method-from-viewcontroller

http://stackoverflow.com/questions/1071112/uiviews-frame-bounds-center-origin-when-to-use-what

http://stackoverflow.com/questions/5361369/uiview-frame-bounds-and-center

http://stackoverflow.com/questions/8564833/ios-upload-image-and-text-using-http-post

http://stackoverflow.com/questions/18226267/changing-root-view-controller-after-ios-app-has-loaded

http://stackoverflow.com/questions/9984859/backbone-js-can-one-view-trigger-updates-in-other-views

http://stackoverflow.com/questions/5392344/sending-multipart-formdata-with-jquery-ajax

http://www.codeschool.com

http://www.bignerdranch.com/we-write/ios-programming/