Building an iOS App with AFIncrementalStore and the Core Data Buildpack

Last Updated: 09 January 2014

afincrementalstore afnetworking core data ios mobile

This article is a work in progress, or documents a feature that is not yet released to all users. This article is unlisted. Only those with the link can access it.

Table of Contents

The Core Data Buildpack has been deprecated. Please refer to the documentation for Helios, an open-source framework that provides essential backend services for iOS apps, including the same data synchronization featured in this article.

Heroku makes it easy to develop mobile applications. With AFIncrementalStore and the Core Data Buildpack, developers are able to get working on the core of their application in a matter of minutes, creating a robust webservice scaffolding that is able to generate RESTful APIs from just the project’s Core Data model.

This article will guide you through the process of developing a small ToDo app. Simple as this example may be, the rapid development strategies described in this article can be applied to networked iOS apps of almost any size or complexity.

This article walks you through the process of building an iOS app on Heroku with Core Data, AFNetworking, and AFIncrementalStore.

This article is also available as a screencast, at mobile.heroku.com.

Code for the the completed iOS Client is available on GitHub.

Prerequisites

Create a new project

Open up Xcode and select “File > New ▶ > New Project…”, or use the keyboard shortcut, ⇧⌘N.

New Project Step 1

When prompted to choose a template for your new project, select iOS - Application on the sidebar, and choose the “AFNetworking Application” template. Click “Next” to continue.

The AFNetworking Xcode project template sets up everything your application needs to get started with AFNetworking and AFIncrementalStore, using CocoaPods for dependency management.

New Project Step 2

In the next step, enter your Product Name, Company Identifier, and Class Prefix (optional). For “Device Family”, select “iPhone”. Make sure that the checkboxes for “Use Core Data”, “Use Automatic Reference Counting”, are checked. Click “Next” to continue.

New Project Step 3

Finally, select a directory to save your new project to, check the box to create a local Git repository for this project, and click “Create”.

Install project dependencies

Before you do anything else with the project, you need to install your project dependencies. Open the Terminal and type the following:

$ cd path/to/your/project
$ pod install
Resolving dependencies of `./Podfile'
Resolving dependencies for target `default' (iOS 5.0)
Downloading dependencies
Installing AFIncrementalStore (0.3.1)
Installing AFNetworking (1.0.1)
Generating support files
Integrating `libPods.a' into target `ToDo' of Xcode project `./ToDo.xcodeproj'

CocoaPods downloads AFNetworking & AFIncrementalStore, and creates a build target for the static library that your project will link against.

Following the instructions from CocoaPods, close ToDo.xcodeproj and open ToDo.xcworkspace. This workspace file contains the build targets for your project as well as its dependencies.

Define a data model

Open the Core Data data model file (.xcdatamodel), and create a Task entity. For that Task has two attributes: the text of the task, which is a string, and completedAt, which is a date.

Core Data Model Editor

Using a timestamp rather than boolean for marking state change information allows for auditing of changes over time, and provides greater flexibility in developing future functionality.

Deploy Core Data model to Heroku

Commit your current changes to Git:

$ git commit -a -m "Defining data model"

Create a Heroku app with the Core Data Buildpack:

$ heroku create --buildpack git://github.com/mattt/heroku-buildpack-core-data.git
Creating infinite-journey-9634... done, stack is cedar
BUILDPACK_URL=git://github.com/mattt/heroku-buildpack-core-data.git
http://infinite-journey-9634.herokuapp.com/ | git@heroku.com:infinite-journey-9634.git
Git remote heroku added

And deploy your code:

$ git push heroku master
Counting objects: 34, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (31/31), done.
Writing objects: 100% (34/34), 19.72 KiB, done.
Total 34 (delta 1), reused 0 (delta 0)

-----> Heroku receiving push
-----> Fetching custom git buildpack... done
-----> Core Data app detected
-----> Compiling Core Data model
-----> Setting Locale
       Successfully installed bundler-1.2.2
       ...
       16 gems installed
-----> Discovering process types
       Procfile declares types     -> (none)
       Default types for Core Data -> web
-----> Compiled slug size: 8.4MB
-----> Launching... done, v7
       http://infinite-journey-9634.herokuapp.com deployed to Heroku

To git@heroku.com:infinite-journey-9634.git
 * [new branch]      master -> master

The Core Data buildpack looks for the Core Data model in your project, and automatically generates a REST webservice with endpoints for each entity, as well as nested endpoints for relations.

Your Heroku app has the following endpoints:

Method Path
GET /tasks
POST /tasks
GET /tasks/1
PUT /tasks/1
DELETE /tasks/1

Validate that everything is working by sending a GET request to /tasks with curl:

$ curl -i http://infinite-journey-9634.herokuapp.com/tasks
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Content-Length: 2
Connection: keep-alive

[]

The /tasks endpoint responds with an empty JSON array, which makes sense, because you haven’t created any tasks yet. Do that now, with:

$ curl -i -X POST -d "text=Deploy to Heroku" http://infinite-journey-9634.herokuapp.com/tasks
HTTP/1.1 201 Created
Content-Type: application/json;charset=utf-8
Content-Length: 63
Connection: keep-alive

{"completedAt":null,"text":"Deploy to Heroku","url":"/tasks/1"}

The POST inserted a new row in the Heroku Postgres database that was provisioned with our app, and returns the JSON of the newly-created record.

You can read more about Heroku Postgres in this article.

By making another GET request to /tasks, you can see that the response now includes the task was just created:

$ curl -i http://infinite-journey-9634.herokuapp.com/tasks
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Content-Length: 65
Connection: keep-alive

[{"completedAt":null,"text":"Deploy to Heroku","url":"/tasks/1"}]

Show tasks in a table view

Now that you’ve verified that the server works as expected, it’s time to hook up the client.

Copy the URL of the Heroku app as the baseURL of your HTTP client. AFIncrementalStore will asynchronously make HTTP requests to read and write to this domain.

Next, create a TasksViewController, which will inherit from UITableViewController, and use XIBs for layout. An NSManagedObjectContext property is added to allow the controller to fetch Task records from the application Core Data stack.

TasksViewController.h

#import <UIKit/UIKit.h>

@interface TasksViewController : UITableViewController <UITextFieldDelegate>

@property NSManagedObjectContext *managedObjectContext;

@end

In our AppDelegate, initialize a new TasksViewController and set it as the rootViewController of the navigation controller. Then set the managedObjectContext property to the AppDelegate’s context.

AppDelegate.m

#import "AppDelegate.h"
#import "TasksViewController.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    TasksViewController *viewController = [[TasksViewController alloc] initWithNibName:@"TasksViewController" bundle:nil];
    viewController.managedObjectContext = self.managedObjectContext;
    self.navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];

    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.rootViewController = self.navigationController;
    [self.window makeKeyAndVisible];

    return YES;
}

Back in TasksViewController.m, add a NSFetchedResultsController property in a class extension. Go ahead and add <NSFetchedResultsControllerDelegate> to the class’s protocols now, since the controller will act as the fetched results controller delegate as well.

TasksViewController.m

@interface TasksViewController () <NSFetchedResultsControllerDelegate>
@property NSFetchedResultsController *fetchedResultsController;
@end

Moving into the implementation, begin -viewDidLoad with a call to super, and set the view controller title with an NSLocalizedString. Next, create an NSFetchRequest for the fetched results controller about to be created. The fetch request is interested in the Task entity, and will sort results by completedAt, descending, so that completed tasks stay at the top. Using the fetch request, create the fetched results controller, and set self as its delegate. Finish out the method with a call to performFetch:.

- (void)viewDidLoad {
    [super viewDidLoad];

    self.title = NSLocalizedString(@"ToDo", nil);

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Task"];
    fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"completedAt" ascending:NO]];

    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
    self.fetchedResultsController.delegate = self;
    [self.fetchedResultsController performFetch:nil];
}

Next, implement the required UITableViewDataSource and NSFetchedResultsControllerDelegate protocol methods. This is mostly boiler plate, since NSFetchedResultsController takes care of fetched, grouping, and ordering records. The only thing that’s specific to your app is -configureCell:forRowAtIndexPath:, which sets the textLabel to display the text of the corresponding Task.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]   ;
    }

    [self configureCell:cell forRowAtIndexPath:indexPath];

    return cell;
}

- (void)configureCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSManagedObject *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = [managedObject valueForKey:@"text"];
}

#pragma mark - NSFetchedResultsControllerDelegate

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller
  didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex
     forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]    withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]    withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)object
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]     withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]    withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeUpdate:
            [self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] forRowAtIndexPath:indexPath];
            break;
        case NSFetchedResultsChangeMove:
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]    withRowAnimation:UITableViewRowAnimationAutomatic];
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]     withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}

Click the “Run” play button, or use the keyboard shortcut, ⌘R. If all was successful, the table view should display the task created earlier with curl.

Build and Run Results

AFIncrementalStore automatically downloaded the Tasks from your Heroku app, just based on the fetch request alone, without any explicit networking code. With AFIncrementalStore, you can integrate your Heroku app directly into the Core Data stack. Whether it’s a fetch or save changes request, or fulfilling an attribute or relation fault, AFIncrementalStore handles all of the networking needed to read and write to and from the server.

Mark tasks as completed

Now is a good time to create a proper model class. As the corresponding class for the Task entity, it will inherit from NSManagedObject, and define properties for the text and completedAt attributes. Additionally, a custom property, completed, is defined, which will encapsulate the logic around marking tasks as completed:

Task.h

#import <CoreData/CoreData.h>

@interface Task : NSManagedObject

@property NSString *text;
@property NSDate *completedAt;

@property (nonatomic, getter = isCompleted) BOOL completed;

@end

In the implementation, use @dynamic to tell the compiler that the implementations for text and completedAtwill be implemented at runtime. As for the custom completed property methods, you’ll implement those now. isCompleted is fairly simple, if it has a value, return YES, otherwise, return NO. For setCompleted:, we’ll either set completedAt to now, if YES, or nil if NO.

Task.m

#import "Task.h"

@implementation Task
@dynamic text;
@dynamic completedAt;

- (BOOL)isCompleted {
    return self.completedAt != nil;
}

- (void)setCompleted:(BOOL)completed {
    self.completedAt = completed ? [NSDate date] : nil;
}

@end

With the class in place, tell the Task entity to use this new class:

Task Entity Class

Back in TasksViewController, go ahead and rip out the original version for -configureCell:forRowAtIndexPath: re-implement using the new Task class. It’s the same idea as before, but using properties instead of key-value coding. The isCompleted method is used here to conditionally set the text color to light gray for completed tasks:

TasksViewController.m

- (void)configureCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    Task *task = [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = task.text;
    cell.textLabel.textColor = [task isCompleted] ? [UIColor lightGrayColor] : [UIColor blackColor];
}

Next up is the UITableViewDelegate method, -tableView:didSelectRowAtIndexPath:. When the user taps a row, the controller should to toggle the completed state of the corresponding task:

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [self.managedObjectContext performBlock:^{
        Task *task = [self.fetchedResultsController objectAtIndexPath:indexPath];
        task.completed = !task.completed;
        [self.managedObjectContext save:nil];
    }];

    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

Build and Run again to see the ToDo list, same as before, only now if the user taps on the row, the task turns light gray to show that it’s marked as complete.

Build and Run Results

You may have also noticed the network activity indicator showing for a moment. That’s because that update to the Task was just synced to the server. You can verify this with curl:

$ curl -i http://infinite-journey-9634.herokuapp.com/tasks
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Content-Length: 88
Connection: keep-alive

[{"completedAt":"2012-11-16 23:15:15 +0000","text":"Deploy to Heroku","url":"/tasks/1"}]

Now that you’ve marked your task as complete, you’re no doubt ready to add some more tasks to the list. That will be the next and final section in this tutorial.

Add new tasks

Back in TasksViewController.h, add a text field IBOutlet @property, and declare the controller as conforming to the UITextFieldDelegate protocol.

TasksViewController.h

#import <UIKit/UIKit.h>

@interface TasksViewController : UITableViewController <UITextFieldDelegate>

@property NSManagedObjectContext *managedObjectContext;

@property IBOutlet UITextField *taskTextField;

@end

Opening TasksViewContrller.xib, drag out a new UIView for a table header view, and place a text field inside it. Adjust the position, set the font size, and enter placeholder text according to your particular design preferences.

Dragging from the text field to “File’s Owner”, set TasksViewController as the text field delegate. Dragging from “File’s Owner” to the text field, set the IBOutlet @property of TasksViewController.

TasksViewController.xib

There’s one method in UITextFieldDelegate protocol to implement: -textFieldShouldReturn:. This method is called when the user taps the Return key.

First, copy the current text of the control into a variable, so you can clear out the text field and have it resign, thereby dismissing the keyboard. In a performBlock, insert a new Task, set its text to the text we copied from the control, and save the context. Finally, return YES, to signify that the text field should indeed return:

#pragma mark - UITextFieldDelegate

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    NSString *text = [textField.text copy];
    textField.text = nil;
    [textField resignFirstResponder];

    [self.managedObjectContext performBlock:^{
        NSManagedObject *managedObject = [NSEntityDescription insertNewObjectForEntityForName:@"Task"     inManagedObjectContext:self.managedObjectContext];
        [managedObject setValue:text forKey:@"text"];
        [self.managedObjectContext save:nil];
    }];

    return YES;
}

Build and Run, to we see the ToDo list, now with a text field at the top. Type some text and hit return, and a new task is added to the list.

Build and Run Results

When the task was added, you may have seen the network activity icon briefly animate to indicate that the new task was POST created on the server as well.

curl the /tasks endpoint one last time, to see that the new task was indeed created:

$ curl -i http://infinite-journey-9634.herokuapp.com/tasks
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Content-Length: 161
Connection: keep-alive

[{"completedAt":"2012-11-16 23:15:15 +0000","text":"Deploy to Heroku","url":"/tasks/1"},{   "completedAt":null,"text":"Finish Dev Center article","url":"/tasks/2"}]

Summary

From here, there are a number of additional features you could develop, whether that’s the ability to add notes, tags, or multimedia, or an innovative new UI. Or perhaps you’ll feel more inclined to apply the rapid development techniques described in this article to build your dream app.

No matter where your development efforts take you, Heroku will be there to provide the tools and technologies needed to make your mobile application a smash success.