Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Rails + iOS with RestKit
@andrewculver
The Evolution of iOS Tools
Before iCloud
After iCloud
Syncing is ... hard?
Keep it simple.
Topics
• Rails API Versioning
• RestKit Installation
• Mapping Configuration
• Pulling and Pushing Data
• Offline Mode and...
Goals
• Understand some basics.
• Keep it as simple as possible.
Our Example App
“ClipQueue”
“ClipQueue”
• Helps with workflow automation.
• Detects content in the clipboard.
• Prompts to save it as a “Clip”, catego...
Rails JSON API
Rails JSON API
• Keep your API separate from your core app.
• Doing this efficiently requires thin controllers.
• Creating...
class Clip < ActiveRecord::Base
attr_accessible :content, :created_at
end
Clip Model
Clipqueue::Application.routes.draw do
namespace :api do
namespace :v1 do
resources :clips
end
end
end
Setting up a Router
class Api::V1::ClipsController < ApplicationController
load_and_authorize_resource :clip
respond_to :json
...
end
API Cont...
class Api::V1::ClipsController < ApplicationController
load_and_authorize_resource :clip
respond_to :json
...
def index
re...
class Api::V1::ClipsController < ApplicationController
load_and_authorize_resource :clip
respond_to :json
...
def show
res...
class Api::V1::ClipsController < ApplicationController
load_and_authorize_resource :clip
respond_to :json
...
def create
@...
class Ability
include CanCan::Ability
// Guest users can do anything.
def initialize(user)
user ||= User.new
can :manage, ...
RestKit Installation
Installation
• Use CocoaPods.
• This tutorial includes great installation steps for
new projects.
• https://github.com/Res...
Rails API Versioning
Rails JSON API Versioning
• If your API changes, older apps will crash.
• Your ranking in the App Store will crash, too.
•...
$ cp -R v1/ v2/
Setting up a Router
Clipqueue::Application.routes.draw do
namespace :api do
namespace :v1 do
resources :clips
end
namespace :v2 do
resources :...
Pulling Data
RESTKit ‘GET’ Lifecycle
• Issues HTTP GET request for URL.
• Receives XML/JSON response.
• Converts to NSDictionary + NSAr...
RKEntityMapping *clipMapping = [RKEntityMapping
mappingForEntityForName: @"Clip"
inManagedObjectStore: managedObjectStore]...
RKResponseDescriptor *clipsResponseDescriptor =
[RKResponseDescriptor
responseDescriptorWithMapping: clipMapping
pathPatte...
RKResponseDescriptor *clipResponseDescriptor =
[RKResponseDescriptor
responseDescriptorWithMapping: clipMapping
pathPatter...
- (void)loadClips
{
[[RKObjectManager sharedManager]
getObjectsAtPath:@"/api/v1/clips"
parameters:nil
success:nil
failure:...
- (void)loadClips
{
[[RKObjectManager sharedManager]
getObjectsAtPath:@"/api/v1/clips"
parameters:nil
success:
^(RKObjectR...
The View
• How does this information get to the user?
• A NSFetchedResultsController is handling the heavy
lifting.
Pull to Refresh
// In your UITableViewController.
UIRefreshControl *refreshControl = [UIRefreshControl new];
[refreshControl
addTarget:sel...
Pushing Data
RESTKit ‘POST’ Lifecycle
• Issues HTTP POST request to URL.
• Rails creates the object and redirects to it.
• From here it...
Clip *clip = (Clip *)[NSEntityDescription
insertNewObjectForEntityForName:@"Clip"
inManagedObjectContext:self.managedObjec...
[[RKObjectManager sharedManager] postObject:clip
path:@"/api/v1/clips"
parameters:nil
success:nil
failure:nil];
Posting a ...
[[RKObjectManager sharedManager] postObject:clip
path:@"/api/v1/clips"
parameters:nil
success:
^(RKObjectRequestOperation ...
RKRequestDescriptor *newClipRequestDescriptor =
[RKRequestDescriptor
requestDescriptorWithMapping:[clipMapping inverseMapp...
Mass-Assignment
• Rails’ new mass-assignment defaults are good, but
they cause issues for us here.
• RESTKit sends the ‘id...
Offline Mode
Offline Mode
• The server is the authority, but is not always
available.
• We want to be able to work and save data locall...
A VerySimple Example
• In this app, “Clips” are read-only.
• Don’t have to worry about resolving conflicting
changes from ...
A VerySimple Solution
• Try saving to the server.
• If that doesn’t work, just save it locally.
• It won’t have a “Clip ID...
A VerySimple Solution
• Try saving to the server.
• If that doesn’t work, just save it locally.
• It won’t have a “Clip ID...
A VerySimple Solution
• The only question is, how will the new ID get mapped
to the locally stored object?
• It can’t be m...
NSString *dataBaseString = [RKApplicationDataDirectory()
stringByAppendingPathComponent:@"ClipQueue.sqlite"];
NSPersistent...
RKEntityMapping *clipMapping = [RKEntityMapping
mappingForEntityForName: @"Clip"
inManagedObjectStore: managedObjectStore]...
clip.uuid = [self getUUID];
[[RKObjectManager sharedManager] postObject:clip
path:@"/api/v1/clips" parameters:nil
success:...
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:...
class Clip < ActiveRecord::Base
...
before_validation do
self.uuid = UUIDTools::UUID.random_create.to_s if uuid.nil?
end
e...
Sync Friendly Deletes
Sync Friendly Deletes
• An object exists locally, but not on the server.
• Was it created locally or deleted remotely?
• A...
class Clip < ActiveRecord::Base
...
scope :not_deleted, where('deleted_at IS NULL')
def destroy
self.update_column(:delete...
- (NSFetchedResultsController *)fetchedResultsController
{
...
// Filter out deleted results.
NSPredicate *predicate =
[NS...
Added Bonus
• NSFetchedResultsController makes this an other UI
updates really pleasant to the user by default, even
when ...
Authentication
Authentication Goals
• Require username and password.
• Scope all data by user.
Authentication
• Prompt for credentials.
• Store them in iOS Keychain.
• Delay loading until credentials are provided.
• C...
Devise.setup do |config|
...
config.http_authenticatable = true
...
end
HTTP Authentication
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if ([self shouldDisplayLoginModal]) {
[self perfo...
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (![self shouldDisplayLoginModal]) {
[self loadOb...
RKObjectManager* objectManager = [RKObjectManager sharedManager];
objectManager.client.username = emailAddressField.text;
...
Add User-Clips Relationship
• Add “user_id” attribute to Clip.
• Add “has_one :user” relationship to Clip.
• Add “has_many...
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.persisted?
can :manage, :clip, {user_...
class Api::V1::ClipsController < ApplicationController
before_filter :authenticate_user!
load_and_authorize_resource :clip...
What We’ve Covered
• Rails API Versioning
• RestKit Installation
• Mapping Configuration
• Pulling and Pushing Data
• Offl...
Goals
• Understand some basics.
• Keep it as simple as possible.
Additional Resources
Blake Watters’ RESTKit 0.20.0 “Gist” Tutorial
https://github.com/RestKit/RKGist/blob/master/TUTORIAL....
Thanks! Questions?
@andrewculver
Rails and iOS with RestKit
Rails and iOS with RestKit
Rails and iOS with RestKit
Rails and iOS with RestKit
Rails and iOS with RestKit
Rails and iOS with RestKit
Rails and iOS with RestKit
Rails and iOS with RestKit
Upcoming SlideShare
Loading in …5
×

Rails and iOS with RestKit

5,012 views

Published on

Published in: Technology, Education

Rails and iOS with RestKit

  1. 1. Rails + iOS with RestKit @andrewculver
  2. 2. The Evolution of iOS Tools
  3. 3. Before iCloud
  4. 4. After iCloud
  5. 5. Syncing is ... hard?
  6. 6. Keep it simple.
  7. 7. Topics • Rails API Versioning • RestKit Installation • Mapping Configuration • Pulling and Pushing Data • Offline Mode and Other Syncing • Authentication
  8. 8. Goals • Understand some basics. • Keep it as simple as possible.
  9. 9. Our Example App “ClipQueue”
  10. 10. “ClipQueue” • Helps with workflow automation. • Detects content in the clipboard. • Prompts to save it as a “Clip”, categorize it. • Server processes it. • Dispatches webhooks, sends emails, etc.
  11. 11. Rails JSON API
  12. 12. Rails JSON API • Keep your API separate from your core app. • Doing this efficiently requires thin controllers. • Creating resources to represent actions that would otherwise be custom controller actions helps, too.
  13. 13. class Clip < ActiveRecord::Base attr_accessible :content, :created_at end Clip Model
  14. 14. Clipqueue::Application.routes.draw do namespace :api do namespace :v1 do resources :clips end end end Setting up a Router
  15. 15. class Api::V1::ClipsController < ApplicationController load_and_authorize_resource :clip respond_to :json ... end API Controller
  16. 16. class Api::V1::ClipsController < ApplicationController load_and_authorize_resource :clip respond_to :json ... def index respond_with @clips end ... end API Controller Actions
  17. 17. class Api::V1::ClipsController < ApplicationController load_and_authorize_resource :clip respond_to :json ... def show respond_with @clip end ... end API Controller Actions
  18. 18. class Api::V1::ClipsController < ApplicationController load_and_authorize_resource :clip respond_to :json ... def create @clip.save redirect_to [:api, :v1, @clip] end ... end API Controller Actions
  19. 19. class Ability include CanCan::Ability // Guest users can do anything. def initialize(user) user ||= User.new can :manage, :all end end Single User System
  20. 20. RestKit Installation
  21. 21. Installation • Use CocoaPods. • This tutorial includes great installation steps for new projects. • https://github.com/RestKit/RKGist/blob/master/ TUTORIAL.md
  22. 22. Rails API Versioning
  23. 23. Rails JSON API Versioning • If your API changes, older apps will crash. • Your ranking in the App Store will crash, too. • Does anyone have a Gem they use for this? • Maintaining old versions of your API is easy: • Make a separate copy for each new version.
  24. 24. $ cp -R v1/ v2/ Setting up a Router
  25. 25. Clipqueue::Application.routes.draw do namespace :api do namespace :v1 do resources :clips end namespace :v2 do resources :clips resources :categories end end end Setting up a Router
  26. 26. Pulling Data
  27. 27. RESTKit ‘GET’ Lifecycle • Issues HTTP GET request for URL. • Receives XML/JSON response. • Converts to NSDictionary + NSArray structure. • Matches response URL to defined entity mapping. • Maps response data to entity.
  28. 28. RKEntityMapping *clipMapping = [RKEntityMapping mappingForEntityForName: @"Clip" inManagedObjectStore: managedObjectStore]; [clipMapping addAttributeMappingsFromDictionary:@{ @"id": @"clipId", @"content": @"content", @"created_at": @"createdAt"}]; A Mapping
  29. 29. RKResponseDescriptor *clipsResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping: clipMapping pathPattern: @"/api/v1/clips" keyPath: nil statusCodes: RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]; [objectManager addResponseDescriptor:clipsResponseDescriptor]; ‘Index’ Response Descriptor
  30. 30. RKResponseDescriptor *clipResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping: clipMapping pathPattern: @"/api/v1/clips/:id" keyPath: nil statusCodes: RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]; [objectManager addResponseDescriptor:clipResponseDescriptor]; ‘Show’ Response Descriptor
  31. 31. - (void)loadClips { [[RKObjectManager sharedManager] getObjectsAtPath:@"/api/v1/clips" parameters:nil success:nil failure:nil]; } Fetching Objects (UITableViewController)
  32. 32. - (void)loadClips { [[RKObjectManager sharedManager] getObjectsAtPath:@"/api/v1/clips" parameters:nil success: ^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {} failure: ^(RKObjectRequestOperation *operation, NSError *error) {} ]; } Fetching Objects (UITableViewController)
  33. 33. The View • How does this information get to the user? • A NSFetchedResultsController is handling the heavy lifting.
  34. 34. Pull to Refresh
  35. 35. // In your UITableViewController. UIRefreshControl *refreshControl = [UIRefreshControl new]; [refreshControl addTarget:self action:@selector(loadClips) forControlEvents:UIControlEventValueChanged]; self.refreshControl = refreshControl; Adding Pull to Refresh (UITableViewController)
  36. 36. Pushing Data
  37. 37. RESTKit ‘POST’ Lifecycle • Issues HTTP POST request to URL. • Rails creates the object and redirects to it. • From here it’s the same as before: • Receives XML/JSON response. • Converts to NSDictionary + NSArray structure. • Matches redirected URL to defined entity mapping. • Maps response data to entity.
  38. 38. Clip *clip = (Clip *)[NSEntityDescription insertNewObjectForEntityForName:@"Clip" inManagedObjectContext:self.managedObjectContext]; clip.content = [[UIPasteboard generalPasteboard] string]; Creating a New Clip
  39. 39. [[RKObjectManager sharedManager] postObject:clip path:@"/api/v1/clips" parameters:nil success:nil failure:nil]; Posting a New Clip
  40. 40. [[RKObjectManager sharedManager] postObject:clip path:@"/api/v1/clips" parameters:nil success: ^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {} failure: ^(RKObjectRequestOperation *operation, NSError *error) {} ]; Posting a New Clip
  41. 41. RKRequestDescriptor *newClipRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:[clipMapping inverseMapping] objectClass:[Clip class] rootKeyPath:@"clip"]; [objectManager addRequestDescriptor:newClipRequestDescriptor]; A Request Descriptor
  42. 42. Mass-Assignment • Rails’ new mass-assignment defaults are good, but they cause issues for us here. • RESTKit sends the ‘id’ and ‘created_at’ attributes across in the request. • This triggers an exception in development by default, because of this: config.active_record.mass_assignment_sanitizer = :strict
  43. 43. Offline Mode
  44. 44. Offline Mode • The server is the authority, but is not always available. • We want to be able to work and save data locally even in the absence of an Internet connection. • We don’t want to lose our work. • When a connection is available, we’d like our local work to be pushed up to the server.
  45. 45. A VerySimple Example • In this app, “Clips” are read-only. • Don’t have to worry about resolving conflicting changes from two sources. • How simple a solution can you get away with?
  46. 46. A VerySimple Solution • Try saving to the server. • If that doesn’t work, just save it locally. • It won’t have a “Clip ID”, but that doesn’t matter for CoreData, even with relationships. • When a connection becomes available, push it up.
  47. 47. A VerySimple Solution • Try saving to the server. • If that doesn’t work, just save it locally. • It won’t have a “Clip ID”, but that doesn’t matter for CoreData, even with relationships. • When a connection becomes available: • Push it up to the server. • Claim our shiny new “Clip ID”.
  48. 48. A VerySimple Solution • The only question is, how will the new ID get mapped to the locally stored object? • It can’t be matched by primary key ID, because it didn’t have one locally. • We’ll use a globally unique ID.
  49. 49. NSString *dataBaseString = [RKApplicationDataDirectory() stringByAppendingPathComponent:@"ClipQueue.sqlite"]; NSPersistentStore __unused *persistentStore = [managedObjectStore addSQLitePersistentStoreAtPath:dataBaseString fromSeedDatabaseAtPath:nil withConfiguration:nil options:nil error:nil]; Adding Local Storage
  50. 50. RKEntityMapping *clipMapping = [RKEntityMapping mappingForEntityForName: @"Clip" inManagedObjectStore: managedObjectStore]; [clipMapping addAttributeMappingsFromDictionary:@{ @"id": @"clipId", @"content": @"content", @"uuid": @"uuid", @"created_at": @"createdAt", @"deleted_at": @"deletedAt"}]; clipMapping.identificationAttributes = @[ @"uuid" ]; An Improved Mapping
  51. 51. clip.uuid = [self getUUID]; [[RKObjectManager sharedManager] postObject:clip path:@"/api/v1/clips" parameters:nil success: ^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {} failure: ^(RKObjectRequestOperation *operation, NSError *error) { NSError *localError = nil; if (![self.managedObjectContext save:&localError]) { // Show UIAlertView to user. [self showErrorAlert:[localError localizedDescription]]; } }]; Queuing Up Locally
  52. 52. NSFetchRequest *request = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"Clip" inManagedObjectContext:self.managedObjectContext]; [request setEntity:entity]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"clipId == 0"]; [request setPredicate:predicate]; ... for (Clip *clip in mutableFetchResults) { [[RKObjectManager sharedManager] postObject:clip path:@"/api/v1/clips" parameters:nil success:nil failure: nil]; } Syncing Up
  53. 53. class Clip < ActiveRecord::Base ... before_validation do self.uuid = UUIDTools::UUID.random_create.to_s if uuid.nil? end end Updating API Logic
  54. 54. Sync Friendly Deletes
  55. 55. Sync Friendly Deletes • An object exists locally, but not on the server. • Was it created locally or deleted remotely? • An object exists remotely, but not locally. • Was it created remotely or deleted locally? • Let’s look at a really easy way to deal with this.
  56. 56. class Clip < ActiveRecord::Base ... scope :not_deleted, where('deleted_at IS NULL') def destroy self.update_column(:deleted_at, Time.zone.now) end end Soft Delete
  57. 57. - (NSFetchedResultsController *)fetchedResultsController { ... // Filter out deleted results. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"deletedAt == nil"]; [fetchRequest setPredicate:predicate]; ... } Filtering Deleted Items
  58. 58. Added Bonus • NSFetchedResultsController makes this an other UI updates really pleasant to the user by default, even when they’re triggered by results from the server.
  59. 59. Authentication
  60. 60. Authentication Goals • Require username and password. • Scope all data by user.
  61. 61. Authentication • Prompt for credentials. • Store them in iOS Keychain. • Delay loading until credentials are provided. • Configure the credentials for RestKit. • Implement data scoping on the server based on the session.
  62. 62. Devise.setup do |config| ... config.http_authenticatable = true ... end HTTP Authentication
  63. 63. - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if ([self shouldDisplayLoginModal]) { [self performSegueWithIdentifier:@"ShowLogin" sender:self]; } } Show Sign-In Modal
  64. 64. - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (![self shouldDisplayLoginModal]) { [self loadObjectsFromDataStore]; } } Delay Loading
  65. 65. RKObjectManager* objectManager = [RKObjectManager sharedManager]; objectManager.client.username = emailAddressField.text; objectManager.client.password = passwordField.text; Configure Credentials
  66. 66. Add User-Clips Relationship • Add “user_id” attribute to Clip. • Add “has_one :user” relationship to Clip. • Add “has_many :clips” relationship to User.
  67. 67. class Ability include CanCan::Ability def initialize(user) user ||= User.new if user.persisted? can :manage, :clip, {user_id: user.id} end end end HTTP Authentication
  68. 68. class Api::V1::ClipsController < ApplicationController before_filter :authenticate_user! load_and_authorize_resource :clip, through: current_user ... end Scoping Controller Results
  69. 69. What We’ve Covered • Rails API Versioning • RestKit Installation • Mapping Configuration • Pulling and Pushing Data • Offline Mode and Other Syncing • Authentication
  70. 70. Goals • Understand some basics. • Keep it as simple as possible.
  71. 71. Additional Resources Blake Watters’ RESTKit 0.20.0 “Gist” Tutorial https://github.com/RestKit/RKGist/blob/master/TUTORIAL.md (A work in progress.)
  72. 72. Thanks! Questions? @andrewculver

×