Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Syncing CoreData and Web Apps with RestKit

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers. →

Syncing CoreData and Web Apps with RestKit

This presentation discusses (and provides example code for) the following things you'll want to think about when integrating your iOS app with a Rails-based API:

— Rails API Versioning
— RestKit Installation
— Mapping Configuration
— Pulling and Pushing Data
— Offline Mode and Other Syncing
— User Authentication

Avatar for Andrew Culver

Andrew Culver

April 13, 2013

More Decks by Andrew Culver

Other Decks in Programming

Transcript

  1. Topics • Rails API Versioning • RestKit Installation • Mapping

    Configuration • Pulling and Pushing Data • Offline Mode and Other Syncing • Authentication
  2. “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.
  3. 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.
  4. 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
  5. class Ability include CanCan::Ability // Guest users can do anything.

    def initialize(user) user ||= User.new can :manage, :all end end Single User System
  6. Installation • Use CocoaPods. • This tutorial includes great installation

    steps for new projects. • https://github.com/RestKit/RKGist/blob/master/ TUTORIAL.md
  7. 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.
  8. 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
  9. 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.
  10. RKResponseDescriptor *clipsResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping: clipMapping pathPattern: @"/api/v1/clips" keyPath: nil

    statusCodes: RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]; [objectManager addResponseDescriptor:clipsResponseDescriptor]; ‘Index’ Response Descriptor
  11. RKResponseDescriptor *clipResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping: clipMapping pathPattern: @"/api/v1/clips/:id" keyPath: nil

    statusCodes: RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]; [objectManager addResponseDescriptor:clipResponseDescriptor]; ‘Show’ Response Descriptor
  12. - (void)loadClips { [[RKObjectManager sharedManager] getObjectsAtPath:@"/api/v1/clips" parameters:nil success: ^(RKObjectRequestOperation *operation,

    RKMappingResult *mappingResult) {} failure: ^(RKObjectRequestOperation *operation, NSError *error) {} ]; } Fetching Objects (UITableViewController)
  13. The View • How does this information get to the

    user? • A NSFetchedResultsController is handling the heavy lifting.
  14. // In your UITableViewController. UIRefreshControl *refreshControl = [UIRefreshControl new]; [refreshControl

    addTarget:self action:@selector(loadClips) forControlEvents:UIControlEventValueChanged]; self.refreshControl = refreshControl; Adding Pull to Refresh (UITableViewController)
  15. 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.
  16. 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
  17. 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.
  18. A Very Simple 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?
  19. A Very Simple 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.
  20. A Very Simple 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”.
  21. A Very Simple 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.
  22. NSString *dataBaseString = [RKApplicationDataDirectory() stringByAppendingPathComponent:@"ClipQueue.sqlite"]; NSPersistentStore __unused *persistentStore = [managedObjectStore

    addSQLitePersistentStoreAtPath:dataBaseString fromSeedDatabaseAtPath:nil withConfiguration:nil options:nil error:nil]; Adding Local Storage
  23. 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
  24. 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
  25. 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
  26. 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.
  27. 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
  28. - (NSFetchedResultsController *)fetchedResultsController { ... // Filter out deleted results.

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"deletedAt == nil"]; [fetchRequest setPredicate:predicate]; ... } Filtering Deleted Items
  29. 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.
  30. 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.
  31. - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if ([self shouldDisplayLoginModal]) { [self

    performSegueWithIdentifier:@"ShowLogin" sender:self]; } } Show Sign-In Modal
  32. Add User-Clips Relationship • Add “user_id” attribute to Clip. •

    Add “has_one :user” relationship to Clip. • Add “has_many :clips” relationship to User.
  33. 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
  34. What We’ve Covered • Rails API Versioning • RestKit Installation

    • Mapping Configuration • Pulling and Pushing Data • Offline Mode and Other Syncing • Authentication