Lessons Building a Simple iOS + Backbone.js + Ruby on Rails App
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