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

Syncing CoreData and Web Apps with RestKit

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

Andrew Culver

April 13, 2013
Tweet

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