Skip to main content
iPhone on Rails




    Mike Clark clarkware.com
(or Conventions Matter)
Hosted Rails App?


Fielded iPhone App?


     Neither?!
Rails
Installation




               $ sudo gem update --system
               $ sudo gem install rails
               $ sudo gem update rake
               $ sudo gem update sqlite3-ruby
Rails
Scaffold App




         $ rails expenses
         $ cd expenses


         $ script/generate scaffold expense 
                name:string amount:decimal


         $ rake db:migrate

         $ script/server



               http://localhost:3000/expenses
Rails Resource
Routes




     ActionController::Routing::Routes.draw do |map|
       map.resources :expenses
     end
Rails Resource
CRUD Routing




                 POST     /expenses/3

                 GET      /expenses

                 PUT      /expenses/3

                 DELETE   /expenses/3
Rails Resource
Controller


             class ExpensesController < ApplicationController

                 # GET /expenses
                 def index
                 end

                 # POST /expenses
                 def create
                 end

                 # GET /expenses/3
                 def show
                 end

                 # PUT /expenses/3
                 def update
                 end

                 # DELETE /expenses/3
                 def destroy
                 end

             end
Rails Resource
Model


   class Expense < ActiveRecord::Base
     validates_presence_of :name
     validates_numericality_of :amount,
                               :greater_than_or_equal_to => 0
   end




   Expense.create(:name => "iPod touch", :amount => 249.00)

   Expense.find(3)

   Expense.update(3, :amount => 199.00)

   Expense.destroy(3)
Rails Resource
View




       <p>
         <b>Name:</b>
         <%=h @expense.name %>
       </p>

       <p>
         <b>Amount:</b>
         <%=h @expense.amount %>
       </p>

       <%= link_to 'Edit', edit_expense_path(@expense) %> |
       <%= link_to 'Back', expenses_path %>
Rails
CRUD Conventions




         POST       GET      PUT     DELETE


        create     find     update   destroy


        INSERT     SELECT   UPDATE   DELETE
>> print Expense.find(3).to_xml

<?xml version="1.0" encoding="UTF-8"?>
<expense>
  <id type="integer">2</id>
  <name>iPod touch</name>
  <amount type="decimal">199.0</amount>
  <created-at type="datetime">2009-09-15T16:01:54Z</created-at>
  <updated-at type="datetime">2009-09-15T16:01:54Z</updated-at>
</expense>




>> print Expense.find(3).to_json

{"expense":
  {"id":2,
    "name":"iPod touch",
    "amount":199.0,
    "created_at":"2009-09-15T16:01:54Z",
    "updated_at":"2009-09-15T16:01:54Z"
  }
}
Rails Resource
Multi-Format Responses



      class ExpensesController < ApplicationController

         def show
           @expense = Expense.find(params[:id])

           respond_to do   |format|
             format.html   # show.html.erb
             format.xml    { render :xml => @expense }
             format.json   { render :json => @expense }
           end
         end

      end




               GET /expenses/3.{html|xml|json}
Rails
Security Tip




       class User < ActiveRecord::Base

          def to_xml(options={})
            options[:only] = [:id, :name, :screen_name]
            super(options)
          end

       end
iPhone
Scaffolding
iPhone
CRUD
iPhone CRUD
Roll Your Own

@implementation Expense

static NSString *siteURL = @"http://localhost:3000";

- (NSString *)params {
    return [NSString stringWithFormat:@"{"expense":{"name":"%@","amount":"%@"}}",
            self.name, self.amount];
}

- (void)createRemote {
    NSString *url = [NSString stringWithFormat:@"%@/expenses.json", siteURL];
    [Resource post:[self params] to:url];
}

+ (NSArray *)findAllRemote {
    NSString *url = [NSString stringWithFormat:@"%@/expenses.json", siteURL];
    NSString *jsonString = [Resource get:url];
    NSArray *expenses = // create expenses from JSON string
    return expenses;
}

- (void)updateRemote {
    NSString *url = [NSString stringWithFormat:@"%@/expenses/%@.json", siteURL, self.expenseId];
    [Resource put:[self params] to:url];
}

- (void)destroyRemote {
    NSString *url = [NSString stringWithFormat:@"%@/expenses/%@.json", siteURL, self.expenseId];
    [Resource delete:url];
}

@end
HTTPRiot

 Supports generic RESTful operations
 Converts JSON response to NSDictionary
 You decide what to do with the data
 Asynchronous



          http://github.com/Caged/httpriot
HTTPRiot
Configuration and Requests


[HRRestModel setDelegate:someObject];
[HRRestModel setBaseURL:[NSURL URLWithString:@"http://your-server/api"]];


[HRRestModel getPath:@"/expenses.json" withOptions:nil object:nil];



NSDictionary *options =
  [NSDictionary dictionaryWithObject:[expense JSONRepresentation] forKey:@"body"];
[HRRestModel postPath:@"/expenses" withOptions: options object:nil];



NSDictionary * options =
  [NSDictionary dictionaryWithObject:[expense JSONRepresentation] forKey:@"body"];
[HRRestModel putPath:@"/expenses/3" withOptions:options object:nil];




[HRRestModel deletePath:@"/expenses/3" withOptions:nil object:nil];
HTTPRiot
Callbacks


    - (void)restConnection:(NSURLConnection *)connection
         didReturnResource:(id)resource
                    object:(id)object {

        for (id item in resource) {
            Expense *expense = [[Expense alloc] initWithDictionary:item];
            // do something with expenses
        }
    }

    - (void)restConnection:(NSURLConnection *)connection
          didFailWithError:(NSError *)error
                    object:(id)object {
        // Handle connection errors
    }

    - (void)restConnection:(NSURLConnection *)connection
           didReceiveError:(NSError *)error
                  response:(NSHTTPURLResponse *)response
                    object:(id)object {
        // Handle invalid responses: 404, 500, and so on
    }

    - (void)restConnection:(NSURLConnection *)connection
      didReceiveParseError:(NSError *)error
              responseBody:(NSString *)string {
        // Request was successful, but couldn't parse the data returned by the server
    }
ASIHTTPRequest

 Super flexible with lots of features
 File uploads, progress indictors, etc.
 Uses CFNetwork API
 Asynchronous


        http://github.com/pokeb/asi-http-request
ASIHTTPRequest
Usage




 - (void)sendRequest {
    NSURL *url = [NSURL URLWithString:@"http://your-server.com/expenses.json"];
    ASIHTTPRequest *request = [[ASIHTTPRequest alloc] initWithURL:url];
    [request setDelegate:self];
    [request setDidFinishSelector:@selector(requestSucceeded:)];
    [request setDidFailSelector:@selector(requestFailed:)];
    [[self queue] addOperation:request];
    [request release];
 }

 - (void)requestSucceeded:(ASIHTTPRequest *)request {
    NSString *response = [request responseString];
 }

 - (void)requestFailed:(ASIHTTPRequest *)request {
    NSError *error = [request error];
 }
ObjectiveResource

 Objective-C port of ActiveResource
 Serializes to/from objects (XML or JSON)
 Assumes Rails RESTful conventions
 Asynchronous option in 1.1 branch


      http://github.com/yfactorial/objectiveresource
ObjectiveResource
Remote Resource


         #import "ObjectiveResource.h"

         @interface Expense : NSObject {
             NSString *expenseId;
             NSString *name;
             NSString *amount;
             NSDate   *createdAt;
             NSDate   *updatedAt;
         }

         @property   (nonatomic,   copy)     NSString   *expenseId;
         @property   (nonatomic,   copy)     NSString   *name;
         @property   (nonatomic,   copy)     NSString   *amount;
         @property   (nonatomic,   retain)   NSDate     *createdAt;
         @property   (nonatomic,   retain)   NSDate     *updatedAt;

         @end




                                                  * NSObject+ObjectiveResource category
ObjectiveResource
Configuration




 #import "ObjectiveResource.h"

 [ObjectiveResourceConfig setSite:@"http://your-server.com/"];
 [ObjectiveResourceConfig setResponseType:JSONResponse];
ObjectiveResource
CRUD Operations




        NSArray *expenses = [Expense findAllRemote];


        Expense *expense = [[Expense alloc] init];
        expense.name = @"iPod touch";
        [expense createRemote];


        Expense *expense = [Expense findRemote:@"3"];
        expense.name = @"iPhone";
        [expense updateRemote];



        [expense destroyRemote];
ObjectiveResource
Asynchronous Way




     [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
     [[ConnectionManager sharedInstance] runJob:@selector(loadExpenses)
                                       onTarget:self];



                                                * NSInvocationOperation in an NSOperationQueue




 - (void)loadExpenses {
     self.expenses = [Expense findAllRemote];

       [self.tableView performSelectorOnMainThread:@selector(reloadData)
                                        withObject:nil
                                     waitUntilDone:NO];

       [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
 }
iPhone
Nested Resources
Rails
Nested Resources




     ActionController::Routing::Routes.draw do |map|
       map.resources :budgets, :has_many => :expenses
     end
Rails
Nested Resource


      class ExpensesController < ApplicationController

        before_filter :find_budget

        def show
          @expense = @budget.expenses.find(params[:id])

          respond_to do |format|
            format.html
            format.xml { render :xml => @expense }
            format.json { render :json => @expense }
          end
        end

      private

        def find_budget
          @budget = current_user.budgets.find(params[:budget_id])
        end

      end


                   GET /budgets/6/expenses
ObjectiveResource
Nested Resources




    @implementation Budget

    @synthesize budgetId;

    - (NSArray *)findAllExpenses {
      return [Expense findRemote:[NSString stringWithFormat:@"%@/%@",
                                      self.budgetId, @"expenses"]];
    }

    @end




                    GET /budgets/6/expenses
ObjectiveResource
Nested Resources

     @implementation Expense

     @synthesize expenseId;
     @synthesize budgetId;

     + (NSString *)getRemoteCollectionName {
         return @"budgets";
     }

     - (NSString *)nestedPath {
         NSString *path = [NSString stringWithFormat:@"%@/expenses", self.budgetId];
         if (self.expenseId) {
             path = [path stringByAppendingFormat:@"/%@", self.expenseId];
         }
         return path;
     }

     - (BOOL)createRemoteWithResponse:(NSError **)aError {
         return [self createRemoteAtPath:[[self class] getRemoteElementPath:[self nestedPath]]
                            withResponse:aError];
     }

     - (BOOL)updateRemoteWithResponse:(NSError **)aError {
         return [self updateRemoteAtPath:[[self class] getRemoteElementPath:[self nestedPath]]
                            withResponse:aError];
     }

     - (BOOL)destroyRemoteWithResponse:(NSError **)aError {
         return [self destroyRemoteAtPath:[[self class] getRemoteElementPath:[self nestedPath]]
                             withResponse:aError];
     }

     @end
                                                                               * yeah, itʼs kinda clunky
iPhone & Rails
Authentication
Rails
Authentication



   class BudgetsController < ApplicationController

      before_filter :authenticate

      def index
        @budgets = current_user.budgets.all
      end

      def show
        @budget = current_user.budgets.find(params[:id])
      end

   private

      def current_user
        @current_user ||= User.find(session[:user_id])
      end

   end
Rails
Authentication


     def authenticate
       return if session[:user_id]

       respond_to do |format|
         format.html do
           redirect_to login_url
         end
         format.any(:xml, :json) do
           user = authenticate_with_http_basic do |username, password|
             User.authenticate(username, password)
           end
           if user
             session[:user_id] = user.id
           else
             request_http_basic_authentication
           end
         end
       end
     end
ObjectiveResource
Configuration




#import "ObjectiveResource.h"

[ObjectiveResourceConfig   setSite:@"http://your-server.com/"];
[ObjectiveResourceConfig   setResponseType:JSONResponse];
[ObjectiveResourceConfig   setUser:@"John"];
[ObjectiveResourceConfig   setPassword:@"Appleseed"];
RESTful web services are
resource based, stateless,
      and scaleable
http://github.com/clarkware
Hands-on iPhone and Rails
training from the folks who
      wrote the books

       pragmaticstudio.com