I Phone On Rails

39
iPhone on Rails Mike Clark clarkware.com

description

iPhone applications can often benefit by talking to a web service to synchronize data or share information with a community. Ruby on Rails, with its RESTful conventions, is an ideal backend for iPhone applications. In this session you'll learn how to use ObjectiveResource in an iPhone application to interact with a RESTful web service implemented in Rails. This session isn't about how to build web applications that are served up on the iPhone. It's about how to build iPhone applications with a native look and feel that happen to talk to Rails applications under the hood. The upshot is a user experience that transcends the device.

Transcript of I Phone On Rails

Page 1: I Phone On Rails

iPhone on Rails

Mike Clark clarkware.com

Page 2: I Phone On Rails

(or Conventions Matter)

Page 3: I Phone On Rails

Hosted Rails App?

Fielded iPhone App?

Neither?!

Page 4: I Phone On Rails

RailsInstallation

$ sudo gem update --system

$ sudo gem install rails

$ sudo gem update rake

$ sudo gem update sqlite3-ruby

Page 5: I Phone On Rails

$ rails expenses

$ cd expenses

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

$ rake db:migrate

$ script/server

http://localhost:3000/expenses

RailsScaffold App

Page 6: I Phone On Rails

ActionController::Routing::Routes.draw do |map| map.resources :expensesend

Rails ResourceRoutes

Page 7: I Phone On Rails

POST

GET

PUT

DELETE

/expenses

/expenses/3

/expenses/3

/expenses/3

Rails ResourceCRUD Routing

Page 8: I Phone On Rails

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 ResourceController

Page 9: I Phone On Rails

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

Expense.find(3)

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

Expense.destroy(3)

Rails ResourceModel

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

Page 10: I Phone On Rails

Rails ResourceView

<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 %>

Page 11: I Phone On Rails

GET

find

SELECT

POST PUT DELETE

create

INSERT

update

UPDATE

destroy

DELETE

RailsCRUD Conventions

Page 12: I Phone On Rails

>> 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" }}

Page 13: I Phone On Rails

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 ResourceMulti-Format Responses

Page 14: I Phone On Rails

class User < ActiveRecord::Base def to_xml(options={}) options[:only] = [:id, :name, :screen_name] super(options) end end

RailsSecurity Tip

Page 15: I Phone On Rails

iPhoneScaffolding

Page 16: I Phone On Rails

iPhoneCRUD

Page 17: I Phone On Rails

iPhone CRUDRoll 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

Page 18: I Phone On Rails

http://github.com/Caged/httpriot

HTTPRiot

Supports generic RESTful operationsConverts JSON response to NSDictionary

AsynchronousYou decide what to do with the data

Page 19: I Phone On Rails

[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];

HTTPRiotConfiguration and Requests

Page 20: I Phone On Rails

HTTPRiotCallbacks

- (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}

Page 21: I Phone On Rails

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

ASIHTTPRequest

Super flexible with lots of features

Uses CFNetwork API

File uploads, progress indictors, etc.

Asynchronous

Page 22: I Phone On Rails

ASIHTTPRequestUsage

- (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];}

Page 23: I Phone On Rails

http://github.com/yfactorial/objectiveresource

ObjectiveResource

Objective-C port of ActiveResource

Assumes Rails RESTful conventions

Serializes to/from objects (XML or JSON)

Asynchronous option in 1.1 branch

Page 24: I Phone On Rails

ObjectiveResourceRemote 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

Page 25: I Phone On Rails

ObjectiveResourceConfiguration

#import "ObjectiveResource.h" [ObjectiveResourceConfig setSite:@"http://your-server.com/"];[ObjectiveResourceConfig setResponseType:JSONResponse];

Page 26: I Phone On Rails

ObjectiveResourceCRUD 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];

Page 27: I Phone On Rails

ObjectiveResourceAsynchronous Way

- (void)loadExpenses { self.expenses = [Expense findAllRemote]; [self.tableView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO]; [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;}

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

* NSInvocationOperation in an NSOperationQueue

Page 28: I Phone On Rails

iPhoneNested Resources

Page 29: I Phone On Rails

RailsNested Resources

ActionController::Routing::Routes.draw do |map| map.resources :budgets, :has_many => :expensesend

Page 30: I Phone On Rails

RailsNested 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

Page 31: I Phone On Rails

ObjectiveResourceNested Resources

@implementation Budget

@synthesize budgetId;

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

@end

GET /budgets/6/expenses

Page 32: I Phone On Rails

ObjectiveResourceNested 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

Page 33: I Phone On Rails

iPhone & RailsAuthentication

Page 34: I Phone On Rails

RailsAuthentication

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

Page 35: I Phone On Rails

RailsAuthentication

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

Page 36: I Phone On Rails

ObjectiveResourceConfiguration

#import "ObjectiveResource.h" [ObjectiveResourceConfig setSite:@"http://your-server.com/"];[ObjectiveResourceConfig setResponseType:JSONResponse];[ObjectiveResourceConfig setUser:@"John"];[ObjectiveResourceConfig setPassword:@"Appleseed"];

Page 37: I Phone On Rails

RESTful web services are resource based, stateless,

and scaleable

Page 38: I Phone On Rails

http://github.com/clarkware

Page 39: I Phone On Rails

pragmaticstudio.com

Hands-on iPhone and Rails training from the folks who

wrote the books