Rails and iOS with RestKit

  • 3,763 views
Uploaded on

 

More in: Technology , Education
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
3,763
On Slideshare
0
From Embeds
0
Number of Embeds
0

Actions

Shares
Downloads
0
Comments
0
Likes
10

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. Rails + iOS with RestKit @andrewculver
  • 2. The Evolution of iOS Tools
  • 3. Before iCloud
  • 4. After iCloud
  • 5. Syncing is ... hard?
  • 6. Keep it simple.
  • 7. Topics • Rails API Versioning • RestKit Installation • Mapping Configuration • Pulling and Pushing Data • Offline Mode and Other Syncing • Authentication
  • 8. Goals • Understand some basics. • Keep it as simple as possible.
  • 9. Our Example App “ClipQueue”
  • 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. Rails JSON API
  • 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. class Clip < ActiveRecord::Base attr_accessible :content, :created_at end Clip Model
  • 14. Clipqueue::Application.routes.draw do namespace :api do namespace :v1 do resources :clips end end end Setting up a Router
  • 15. class Api::V1::ClipsController < ApplicationController load_and_authorize_resource :clip respond_to :json ... end API Controller
  • 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. class Api::V1::ClipsController < ApplicationController load_and_authorize_resource :clip respond_to :json ... def show respond_with @clip end ... end API Controller Actions
  • 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. 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. RestKit Installation
  • 21. Installation • Use CocoaPods. • This tutorial includes great installation steps for new projects. • https://github.com/RestKit/RKGist/blob/master/ TUTORIAL.md
  • 22. Rails API Versioning
  • 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. $ cp -R v1/ v2/ Setting up a Router
  • 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. Pulling Data
  • 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. RKEntityMapping *clipMapping = [RKEntityMapping mappingForEntityForName: @"Clip" inManagedObjectStore: managedObjectStore]; [clipMapping addAttributeMappingsFromDictionary:@{ @"id": @"clipId", @"content": @"content", @"created_at": @"createdAt"}]; A Mapping
  • 29. RKResponseDescriptor *clipsResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping: clipMapping pathPattern: @"/api/v1/clips" keyPath: nil statusCodes: RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]; [objectManager addResponseDescriptor:clipsResponseDescriptor]; ‘Index’ Response Descriptor
  • 30. RKResponseDescriptor *clipResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping: clipMapping pathPattern: @"/api/v1/clips/:id" keyPath: nil statusCodes: RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]; [objectManager addResponseDescriptor:clipResponseDescriptor]; ‘Show’ Response Descriptor
  • 31. - (void)loadClips { [[RKObjectManager sharedManager] getObjectsAtPath:@"/api/v1/clips" parameters:nil success:nil failure:nil]; } Fetching Objects (UITableViewController)
  • 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. The View • How does this information get to the user? • A NSFetchedResultsController is handling the heavy lifting.
  • 34. Pull to Refresh
  • 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. Pushing Data
  • 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. Clip *clip = (Clip *)[NSEntityDescription insertNewObjectForEntityForName:@"Clip" inManagedObjectContext:self.managedObjectContext]; clip.content = [[UIPasteboard generalPasteboard] string]; Creating a New Clip
  • 39. [[RKObjectManager sharedManager] postObject:clip path:@"/api/v1/clips" parameters:nil success:nil failure:nil]; Posting a New Clip
  • 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. RKRequestDescriptor *newClipRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:[clipMapping inverseMapping] objectClass:[Clip class] rootKeyPath:@"clip"]; [objectManager addRequestDescriptor:newClipRequestDescriptor]; A Request Descriptor
  • 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. Offline Mode
  • 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. 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. 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. 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. 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. NSString *dataBaseString = [RKApplicationDataDirectory() stringByAppendingPathComponent:@"ClipQueue.sqlite"]; NSPersistentStore __unused *persistentStore = [managedObjectStore addSQLitePersistentStoreAtPath:dataBaseString fromSeedDatabaseAtPath:nil withConfiguration:nil options:nil error:nil]; Adding Local Storage
  • 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. 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. 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. class Clip < ActiveRecord::Base ... before_validation do self.uuid = UUIDTools::UUID.random_create.to_s if uuid.nil? end end Updating API Logic
  • 54. Sync Friendly Deletes
  • 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. 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. - (NSFetchedResultsController *)fetchedResultsController { ... // Filter out deleted results. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"deletedAt == nil"]; [fetchRequest setPredicate:predicate]; ... } Filtering Deleted Items
  • 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. Authentication
  • 60. Authentication Goals • Require username and password. • Scope all data by user.
  • 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. Devise.setup do |config| ... config.http_authenticatable = true ... end HTTP Authentication
  • 63. - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if ([self shouldDisplayLoginModal]) { [self performSegueWithIdentifier:@"ShowLogin" sender:self]; } } Show Sign-In Modal
  • 64. - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (![self shouldDisplayLoginModal]) { [self loadObjectsFromDataStore]; } } Delay Loading
  • 65. RKObjectManager* objectManager = [RKObjectManager sharedManager]; objectManager.client.username = emailAddressField.text; objectManager.client.password = passwordField.text; Configure Credentials
  • 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. 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. class Api::V1::ClipsController < ApplicationController before_filter :authenticate_user! load_and_authorize_resource :clip, through: current_user ... end Scoping Controller Results
  • 69. What We’ve Covered • Rails API Versioning • RestKit Installation • Mapping Configuration • Pulling and Pushing Data • Offline Mode and Other Syncing • Authentication
  • 70. Goals • Understand some basics. • Keep it as simple as possible.
  • 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. Thanks! Questions? @andrewculver