Slide 1

Slide 1 text

Rails + iOS with RestKit @andrewculver

Slide 2

Slide 2 text

The Evolution of iOS Tools

Slide 3

Slide 3 text

Before iCloud

Slide 4

Slide 4 text

After iCloud

Slide 5

Slide 5 text

Syncing is ... hard?

Slide 6

Slide 6 text

Keep it simple.

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Goals • Understand some basics. • Keep it as simple as possible.

Slide 9

Slide 9 text

Our Example App “ClipQueue”

Slide 10

Slide 10 text

“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.

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

Rails JSON API

Slide 19

Slide 19 text

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.

Slide 20

Slide 20 text

class Clip < ActiveRecord::Base attr_accessible :content, :created_at end Clip Model

Slide 21

Slide 21 text

Clipqueue::Application.routes.draw do namespace :api do namespace :v1 do resources :clips end end end Setting up a Router

Slide 22

Slide 22 text

class Api::V1::ClipsController < ApplicationController load_and_authorize_resource :clip respond_to :json ... end API Controller

Slide 23

Slide 23 text

class Api::V1::ClipsController < ApplicationController load_and_authorize_resource :clip respond_to :json ... def index respond_with @clips end ... end API Controller Actions

Slide 24

Slide 24 text

class Api::V1::ClipsController < ApplicationController load_and_authorize_resource :clip respond_to :json ... def show respond_with @clip end ... end API Controller Actions

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

class Ability include CanCan::Ability // Guest users can do anything. def initialize(user) user ||= User.new can :manage, :all end end Single User System

Slide 27

Slide 27 text

RestKit Installation

Slide 28

Slide 28 text

Installation • Use CocoaPods. • This tutorial includes great installation steps for new projects. • https://github.com/RestKit/RKGist/blob/master/ TUTORIAL.md

Slide 29

Slide 29 text

Rails API Versioning

Slide 30

Slide 30 text

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.

Slide 31

Slide 31 text

$ cp -R v1/ v2/ Setting up a Router

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Pulling Data

Slide 34

Slide 34 text

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.

Slide 35

Slide 35 text

RKEntityMapping *clipMapping = [RKEntityMapping mappingForEntityForName: @"Clip" inManagedObjectStore: managedObjectStore]; [clipMapping addAttributeMappingsFromDictionary:@{ @"id": @"clipId", @"content": @"content", @"created_at": @"createdAt"}]; A Mapping

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

- (void)loadClips { [[RKObjectManager sharedManager] getObjectsAtPath:@"/api/v1/clips" parameters:nil success:nil failure:nil]; } Fetching Objects (UITableViewController)

Slide 39

Slide 39 text

- (void)loadClips { [[RKObjectManager sharedManager] getObjectsAtPath:@"/api/v1/clips" parameters:nil success: ^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {} failure: ^(RKObjectRequestOperation *operation, NSError *error) {} ]; } Fetching Objects (UITableViewController)

Slide 40

Slide 40 text

The View • How does this information get to the user? • A NSFetchedResultsController is handling the heavy lifting.

Slide 41

Slide 41 text

Pull to Refresh

Slide 42

Slide 42 text

// In your UITableViewController. UIRefreshControl *refreshControl = [UIRefreshControl new]; [refreshControl addTarget:self action:@selector(loadClips) forControlEvents:UIControlEventValueChanged]; self.refreshControl = refreshControl; Adding Pull to Refresh (UITableViewController)

Slide 43

Slide 43 text

Pushing Data

Slide 44

Slide 44 text

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.

Slide 45

Slide 45 text

Clip *clip = (Clip *)[NSEntityDescription insertNewObjectForEntityForName:@"Clip" inManagedObjectContext:self.managedObjectContext]; clip.content = [[UIPasteboard generalPasteboard] string]; Creating a New Clip

Slide 46

Slide 46 text

[[RKObjectManager sharedManager] postObject:clip path:@"/api/v1/clips" parameters:nil success:nil failure:nil]; Posting a New Clip

Slide 47

Slide 47 text

[[RKObjectManager sharedManager] postObject:clip path:@"/api/v1/clips" parameters:nil success: ^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {} failure: ^(RKObjectRequestOperation *operation, NSError *error) {} ]; Posting a New Clip

Slide 48

Slide 48 text

RKRequestDescriptor *newClipRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:[clipMapping inverseMapping] objectClass:[Clip class] rootKeyPath:@"clip"]; [objectManager addRequestDescriptor:newClipRequestDescriptor]; A Request Descriptor

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Offline Mode

Slide 51

Slide 51 text

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.

Slide 52

Slide 52 text

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?

Slide 53

Slide 53 text

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.

Slide 54

Slide 54 text

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”.

Slide 55

Slide 55 text

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.

Slide 56

Slide 56 text

NSString *dataBaseString = [RKApplicationDataDirectory() stringByAppendingPathComponent:@"ClipQueue.sqlite"]; NSPersistentStore __unused *persistentStore = [managedObjectStore addSQLitePersistentStoreAtPath:dataBaseString fromSeedDatabaseAtPath:nil withConfiguration:nil options:nil error:nil]; Adding Local Storage

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

class Clip < ActiveRecord::Base ... before_validation do self.uuid = UUIDTools::UUID.random_create.to_s if uuid.nil? end end Updating API Logic

Slide 61

Slide 61 text

Sync Friendly Deletes

Slide 62

Slide 62 text

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.

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

- (NSFetchedResultsController *)fetchedResultsController { ... // Filter out deleted results. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"deletedAt == nil"]; [fetchRequest setPredicate:predicate]; ... } Filtering Deleted Items

Slide 65

Slide 65 text

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.

Slide 66

Slide 66 text

Authentication

Slide 67

Slide 67 text

Authentication Goals • Require username and password. • Scope all data by user.

Slide 68

Slide 68 text

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.

Slide 69

Slide 69 text

Devise.setup do |config| ... config.http_authenticatable = true ... end HTTP Authentication

Slide 70

Slide 70 text

- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if ([self shouldDisplayLoginModal]) { [self performSegueWithIdentifier:@"ShowLogin" sender:self]; } } Show Sign-In Modal

Slide 71

Slide 71 text

- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (![self shouldDisplayLoginModal]) { [self loadObjectsFromDataStore]; } } Delay Loading

Slide 72

Slide 72 text

RKObjectManager* objectManager = [RKObjectManager sharedManager]; objectManager.client.username = emailAddressField.text; objectManager.client.password = passwordField.text; Configure Credentials

Slide 73

Slide 73 text

Add User-Clips Relationship • Add “user_id” attribute to Clip. • Add “has_one :user” relationship to Clip. • Add “has_many :clips” relationship to User.

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

class Api::V1::ClipsController < ApplicationController before_filter :authenticate_user! load_and_authorize_resource :clip, through: current_user ... end Scoping Controller Results

Slide 76

Slide 76 text

What We’ve Covered • Rails API Versioning • RestKit Installation • Mapping Configuration • Pulling and Pushing Data • Offline Mode and Other Syncing • Authentication

Slide 77

Slide 77 text

Goals • Understand some basics. • Keep it as simple as possible.

Slide 78

Slide 78 text

Additional Resources Blake Watters’ RESTKit 0.20.0 “Gist” Tutorial https://github.com/RestKit/RKGist/blob/master/TUTORIAL.md (A work in progress.)

Slide 79

Slide 79 text

Thanks! Questions? @andrewculver

Slide 80

Slide 80 text

No content