Sunteți pe pagina 1din 9

6/29/2015

A Complete Core Data Application objc.io

Issue 4: Core Data September 2013

Browse Issue

A Complete Core Data Application

By Chris Eidhof

In this article, we will build a small but complete Core Data backed application.
The application allows you to create nested lists; each list item can have a sublist, allowing you to create very deep hierarchies of items. Instead of using the
Xcode template for Core Data, we will build our stack by hand, in order to fully
understand whats going on. The example code for this application is on GitHub.

In this article
How Will We Build It?
Set Up the Stack
Creating a Model
Create a Store Class

How Will We Build It?


First, we will create a PersistentStack object that, given a Core Data Model and
a filename, returns a managed object context. Then we will build our Core Data
Model. Next, we will create a simple table view controller that shows the root list
of items using a fetched results controller, and add interaction step-by-step, by
adding items, navigating to sub-items, deleting items, and adding undo support.

Add a Table-View Backed by Fetched


Results Controller
Creating the Table Views Data Source
Creating the Table View Controller

Adding Interactivity
Adding Items
Listening to Changes
Using a Collection View

Set Up the Stack


We will create a managed object context for the main queue. In older code, you
might see [[NSManagedObjectContext alloc] init] . These days, you should
use the initWithConcurrencyType: initializer to make it explicit that youre
using the queue-based concurrency model:

Implementing Your Own Fetched


Results Controller
Passing Model Objects Around
Deletion
Add Undo Support
Editing
Reordering

OBJECTIVE-C

SELECT ALL

Saving

- (void)setupManagedObjectContext
Discussion
{
self.managedObjectContext =
[[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.managedObjectContext.persistentStoreCoordinator =
[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
NSError* error;
[self.managedObjectContext.persistentStoreCoordinator
addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:self.storeURL
options:nil
error:&error];
if (error) {
NSLog(@"error: %@", error);
}
self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];
}

Its important to check the error, because this will probably fail a lot during
development. When you change your data model, Core Data detects this and will
not continue. You can also pass in options to instruct Core Data about what to do
in this case, which Martin explains thoroughly in his article about migrations. Note
that the last line adds an undo manager; we will need this later. On iOS, you need
to explicitly add an undo manager, whereas on Mac it is there by default.
This code creates a really simple Core Data Stack: one managed object context,
which has a persistent store coordinator, which has one persistent store. More
http://www.objc.io/issues/4-core-data/full-core-data-application/

1/11

6/29/2015

A Complete Core Data Application objc.io

complicated setups are possible; the most common is to have multiple managed
object contexts (each on a separate queue).

Creating a Model
Creating a model is simple, as we just add a new file to our project, choosing the
Data Model template (under Core Data). This model file will get compiled to a file
with extension .momd , which we will load at runtime to create a
NSManagedObjectModel , which is needed for the persistent store. The source of
the model is simple XML, and in our experience, you typically wont have any
merge problems when checking it into source control. It is also possible to create
a managed object model in code, if you prefer that.
Once you create the model, you can add an Item entity with two attributes:
title , which is a string, and order , which is an integer. Then, you add two
relationships: parent , which relates an item to its parent, and children , which
is a to-many relationship. Set the relationships as the inverse of one another,
which means that if you set a s parent to be b , then b will have a in its children
automatically.
Normally, you could even use ordered relationships, and leave out the order
property entirely. However, they dont play together nicely with fetched results
controllers (which we will use later on). We would either need to reimplement part
of fetched results controllers, or reimplement the ordering, and we chose the
latter.
Now, choose Editor > Create NSManagedObject subclass from the menu, and
create a subclass of NSManagedObject that is tied to this entity. This creates two
files: Item.h and Item.m . There is an extra category in the header file, which we
will delete immediately (it is there for legacy reasons).

Create a Store Class


For our model, we will create a root node that is the start of our item tree. We need
a place to create this root node and to find it later. Therefore, we create a simple
Store class, that does exactly this. It has a managed object context, and one
method rootItem . In our app delegate, we will find this root item at launch and
pass it to our root view controller. As an optimization, you can store the object id
of the item in the user defaults, in order to look it up even faster:
OBJECTIVE-C

SELECT ALL

- (Item*)rootItem
{
NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:@"Item"];
request.predicate = [NSPredicate predicateWithFormat:@"parent = %@", nil];
NSArray* objects = [self.managedObjectContext executeFetchRequest:request error:NULL];
Item* rootItem = [objects lastObject];
if (rootItem == nil) {
rootItem = [Item insertItemWithTitle:nil
parent:nil
inManagedObjectContext:self.managedObjectContext];
}
return rootItem;
}

Adding an item is mostly straightforward. However, we have to set the order


property to be larger than any of the existing items with that parent. The invariant
we will use is that the first child has an order of 0, and every subsequent child
has an order value that is 1 higher. We create a custom method on the Item
class where we put the logic:

http://www.objc.io/issues/4-core-data/full-core-data-application/

2/11

6/29/2015

A Complete Core Data Application objc.io

OBJECTIVE-C

SELECT ALL

+ (instancetype)insertItemWithTitle:(NSString*)title
parent:(Item*)parent
inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
NSUInteger order = parent.numberOfChildren;
Item* item = [NSEntityDescription insertNewObjectForEntityForName:self.entityName
inManagedObjectContext:managedObjectContext];
item.title = title;
item.parent = parent;
item.order = @(order);
return item;
}

The number of children is a very simple method:


OBJECTIVE-C

SELECT ALL

- (NSUInteger)numberOfChildren
{
return self.children.count;
}

To support automatic updates to our table view, we will use a fetched results
controller. A fetched results controller is an object that can manage a fetch
request with a big number of items and is the perfect Core Data companion to a
table view, as we will see in the next section:
OBJECTIVE-C

SELECT ALL

- (NSFetchedResultsController*)childrenFetchedResultsController
{
NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:[self.class entityName]];
request.predicate = [NSPredicate predicateWithFormat:@"parent = %@", self];
request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"order" ascending:YES]];
return [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
}

Add a Table-View Backed by Fetched Results


Controller
Our next step is to create the root view controller: a table view, which gets its data
from an NSFetchedResultsController . The fetched results controller manages
your fetch request, and if you assign a delegate, it also notifies you of any changes
in the managed object context. In practice, this means that if you implement the
delegate methods, you can automatically update your table view when relevant
changes happen in the data model. For example, if you synchronize in a
background thread, and then you store the changes in the database, your table
view will update automatically.

Creating the Table Views Data Source


In our article on lighter view controllers, we demonstrated how to separate out the
data source of a table view. We will do exactly the same for a fetched results
controller; we create a separate class FetchedResultsControllerDataSource
that acts as a table views data source, and by listening to the fetched results
controller, updates the table view automatically.
We initialize the object with a table view, and the initializer looks like this:

http://www.objc.io/issues/4-core-data/full-core-data-application/

3/11

6/29/2015

A Complete Core Data Application objc.io

OBJECTIVE-C

SELECT ALL

- (id)initWithTableView:(UITableView*)tableView
{
self = [super init];
if (self) {
self.tableView = tableView;
self.tableView.dataSource = self;
}
return self;
}

When we set the fetch results controller, we have to make ourselves the delegate,
and perform the initial fetch. It is easy to forget the performFetch: call, and you
will get no results (and no errors):
OBJECTIVE-C

SELECT ALL

- (void)setFetchedResultsController:(NSFetchedResultsController*)fetchedResultsController
{
_fetchedResultsController = fetchedResultsController;
fetchedResultsController.delegate = self;
[fetchedResultsController performFetch:NULL];
}

Because our class implements the UITableViewDataSource protocol, we need to


implement some methods for that. In these two methods we just ask the fetched
results controller for the required information:
OBJECTIVE-C

SELECT ALL

- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView
{
return self.fetchedResultsController.sections.count;
}
- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)sectionIndex
{
id<NSFetchedResultsSectionInfo> section = self.fetchedResultsController.sections[sectionIndex];
return section.numberOfObjects;
}

However, when we need to create cells, it requires some simple steps: we ask the
fetched results controller for the right object, we dequeue a cell from the table
view, and then we tell our delegate (which will be a view controller) to configure
that cell with the object. Now, we have a nice separation of concerns, as the view
controller only has to care about updating the cell with the model object:
OBJECTIVE-C

SELECT ALL

- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
id object = [self.fetchedResultsController objectAtIndexPath:indexPath];
id cell = [tableView dequeueReusableCellWithIdentifier:self.reuseIdentifier
forIndexPath:indexPath];
[self.delegate configureCell:cell withObject:object];
return cell;
}

Creating the Table View Controller


Now, we can create a view controller that displays a list of items using the class
we just created. In the example app, we created a Storyboard, and added a
navigation controller with a table view controller. This automatically sets the view
controller as the data source, which is not what we want. Therefore, in our
viewDidLoad , we do the following:

http://www.objc.io/issues/4-core-data/full-core-data-application/

4/11

6/29/2015

A Complete Core Data Application objc.io

OBJECTIVE-C

SELECT ALL

fetchedResultsControllerDataSource =
[[FetchedResultsControllerDataSource alloc] initWithTableView:self.tableView];
self.fetchedResultsControllerDataSource.fetchedResultsController =
self.parent.childrenFetchedResultsController;
fetchedResultsControllerDataSource.delegate = self;
fetchedResultsControllerDataSource.reuseIdentifier = @"Cell";

In the initializer of the fetched results controller data source, the table views data
source gets set. The reuse identifier matches the one in the Storyboard. Now, we
have to implement the delegate method:
OBJECTIVE-C

SELECT ALL

- (void)configureCell:(id)theCell withObject:(id)object
{
UITableViewCell* cell = theCell;
Item* item = object;
cell.textLabel.text = item.title;
}

Of course, you could do a lot more than just setting the text label, but you get the
point. Now we have pretty much everything in place for showing data, but as there
is no way to add anything yet, it looks pretty empty.

Adding Interactivity
We will add a couple of ways of interacting with the data. First, we will make it
possible to add items. Then we will implement the fetched results controllers
delegate methods to update the table view, and add support for deletion and
undo.

Adding Items
To add items, we steal the interaction design from Clear, which is high on my list
of most beautiful apps. We add a text field as the table views header, and modify
the content inset of the table view to make sure it stays hidden by default, as
explained in Joes scroll view article. As always, the full code is on github, but
heres the relevant call to inserting the item, in textFieldShouldReturn :
OBJECTIVE-C

SELECT ALL

[Item insertItemWithTitle:title
parent:self.parent
inManagedObjectContext:self.parent.managedObjectContext];
textField.text = @"";
[textField resignFirstResponder];

Listening to Changes
The next step is making sure that your table view inserts a row for the newly
created item. There are several ways to go about this, but well use the fetched
results controllers delegate method:
OBJECTIVE-C

SELECT ALL

- (void)controller:(NSFetchedResultsController*)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath*)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath*)newIndexPath
{
if (type == NSFetchedResultsChangeInsert) {
[self.tableView insertRowsAtIndexPaths:@[newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
}

The fetched results controller also calls these methods for deletions, changes,
http://www.objc.io/issues/4-core-data/full-core-data-application/

5/11

6/29/2015

A Complete Core Data Application objc.io

and moves (well implement that later). If you have multiple changes happening at
the same time, you can implement two more methods so that the table view will
animate everything at the same time. For simple single-item insertions and
deletions, it doesnt make a difference, but if you choose to implement syncing at
some time, it makes everything a lot prettier:
OBJECTIVE-C

SELECT ALL

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

Using a Collection View


Its worth noting that fetched results controllers are not at all limited to table
views; you can use them with any kind of view. Because they are index-pathbased, they also work really well with collection views, although you unfortunately
have to jump through some minor hoops in order to make it work perfectly, as a
collection view doesnt have a beginUpdates and endUpdates method, but
rather a single method performBatchUpdates . To deal with this, you can collect
all the updates you get, and then in the controllerDidChangeContent , perform
them all inside the block. Ash Furrow wrote an example of how you could do this.

Implementing Your Own Fetched Results Controller


You dont have to use NSFetchedResultsController . In fact, in a lot of cases it
might make sense to create a similar class that works specifically for your
application. What you can do is subscribe to the
NSManagedObjectContextObjectsDidChangeNotification . You then get a
notification, and the userInfo dictionary will contain a list of the changed
objects, inserted objects, and deleted objects. Then you can process them in any
way you want.

Passing Model Objects Around


Now that we can add and list items, its time to make sure we can make sub-lists.
In the Storyboard, you can create a segue by dragging from a cell to the view
controller. Its wise to give the segue a name, so that it can be identified if we ever
have multiple segues originating from the same view controller.
My pattern for dealing with segues looks like this: first, you try to identify which
segue it is, and for each segue you pull out a separate method that prepares the
destination view controller:
OBJECTIVE-C

SELECT ALL

- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
[super prepareForSegue:segue sender:sender];
if ([segue.identifier isEqualToString:selectItemSegue]) {
[self presentSubItemViewController:segue.destinationViewController];
}
}
- (void)presentSubItemViewController:(ItemViewController*)subItemViewController
{
Item* item = [self.fetchedResultsControllerDataSource selectedItem];
subItemViewController.parent = item;
}

The only thing the child view controller needs is the item. From the item, it can
also get to the managed object context. We get the selected item from our data
source (which looks up the table views selected item index and fetches the
correct item from the fetched results controller). Its as simple as that.
One pattern thats unfortunately very common is having the managed object
http://www.objc.io/issues/4-core-data/full-core-data-application/

6/11

6/29/2015

A Complete Core Data Application objc.io

context as a property on the app delegate, and then always accessing it from
everywhere. This is a bad idea. If you ever want to use a different managed object
context for a certain part of your view controller hierarchy, this will be very hard to
refactor, and additionally, your code will be a lot more difficult to test.
Now, try adding an item in the sub-list, and you will probably get a nice crash. This
is because we now have two fetched results controllers, one for the topmost view
controller, but also one for the root view controller. The latter one tries to update
its table view, which is offscreen, and everything crashes. The solution is to tell
our data source to stop listening to the fetched results controller delegate
methods:
OBJECTIVE-C

SELECT ALL

- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.fetchedResultsControllerDataSource.paused = NO;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
self.fetchedResultsControllerDataSource.paused = YES;
}

One way to implement this inside the data source is setting the fetched results
controllers delegate to nil, so that no updates are received any longer. We then
need to add it after we come out of paused state:
OBJECTIVE-C

SELECT ALL

- (void)setPaused:(BOOL)paused
{
_paused = paused;
if (paused) {
self.fetchedResultsController.delegate = nil;
} else {
self.fetchedResultsController.delegate = self;
[self.fetchedResultsController performFetch:NULL];
[self.tableView reloadData];
}
}

The performFetch will then make sure your data source is up to date. Of course,
a nicer implementation would be to not set the delegate to nil, but instead keep a
list of the changes that happened while in paused state, and update the table view
accordingly after you get out of paused state.

Deletion
To support deletion, we need to take a few steps. First, we need to convince the
table view that we support deletion, and second, we need to delete the object
from core data and make sure our order invariant stays correct.
To allow for swipe to delete, we need to implement two methods in the data
source:
OBJECTIVE-C

SELECT ALL

- (BOOL)tableView:(UITableView*)tableView
canEditRowAtIndexPath:(NSIndexPath*)indexPath
{
return YES;
}
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
id object = [self.fetchedResultsController objectAtIndexPath:indexPath]
[self.delegate deleteObject:object];
}
}

Rather than deleting immediately, we tell our delegate (the view controller) to
http://www.objc.io/issues/4-core-data/full-core-data-application/

7/11

6/29/2015

A Complete Core Data Application objc.io

delete the object. That way, we dont have to share the store object with our data
source (the data source should be reusable across projects), and we keep the
flexibility to do any custom actions. The view controller simply calls
deleteObject: on the managed object context.

However, there are two important problems to solve: what do we do with the
children of the item that we delete, and how do we enforce our order variant?
Luckily, propagating deletion is easy: in our data model, we can choose Cascade
as the delete rule for the children relationship.
For enforcing our order variant, we can override the prepareForDeletion
method, and update all the siblings with a higher order :
OBJECTIVE-C

SELECT ALL

- (void)prepareForDeletion
{
NSSet* siblings = self.parent.children;
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"order > %@", self.order];
NSSet* siblingsAfterSelf = [siblings filteredSetUsingPredicate:predicate];
[siblingsAfterSelf enumerateObjectsUsingBlock:^(Item* sibling, BOOL* stop)
{
sibling.order = @(sibling.order.integerValue - 1);
}];
}

Now were almost there. We can interact with table view cells and delete the
model object. The final step is to implement the necessary code to delete the
table view cells once the model objects get deleted. In our data sources
controller:didChangeObject:... method we add another if clause:
OBJECTIVE-C

SELECT ALL

...
else if (type == NSFetchedResultsChangeDelete) {
[self.tableView deleteRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
}

Add Undo Support


One of the nice things about Core Data is that it comes with integrated undo
support. We will add the shake to undo feature, and a first step is telling the
application that we can do this:
OBJECTIVE-C

SELECT ALL

application.applicationSupportsShakeToEdit = YES;

Now, whenever a shake is triggered, the application will ask the first responder for
its undo manager, and perform an undo. In last months article, we saw that a view
controller is also in the responder chain, and this is exactly what well use. In our
view controller, we override the following two methods from the UIResponder
class:
OBJECTIVE-C

SELECT ALL

- (BOOL)canBecomeFirstResponder {
return YES;
}
- (NSUndoManager*)undoManager
{
return self.managedObjectContext.undoManager;
}

Now, when a shake gesture happens, the managed object contexts undo manager
will get an undo message, and undo the last change. Remember, on iOS, a
managed object context doesnt have an undo manager by default, (whereas on
Mac, a newly created managed object context does have an undo manager), so we
created that in the setup of the persistent stack:

http://www.objc.io/issues/4-core-data/full-core-data-application/

8/11

6/29/2015

A Complete Core Data Application objc.io

OBJECTIVE-C

SELECT ALL

self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];

And thats almost all there is to it. Now, when you shake, you get the default iOS
alert view with two buttons: one for undoing, and one for canceling. One nice
feature of Core Data is that it automatically groups changes. For example, the
addItem:parent will record as one undo action. For the deletion, its the same.
To make managing the undos a bit easier for the user, we can also name the
actions, and change the first lines of textFieldShouldReturn: to this:
BOOKS 3

ISSUES 24

OBJECTIVE-C

Blog

Newsletter

About

Search

SELECT ALL

NSString* title = textField.text;


NSString* actionName = [NSString stringWithFormat:
NSLocalizedString(@"add item \"%@\"", @"Undo action name of add item"), title];
[self.undoManager setActionName:actionName];
[self.store addItem:title parent:nil];

Now, when the user shakes, he or she gets a bit more context than just the generic
label Undo.

Editing
Editing is currently not supported in the example application, but is a matter of
just changing properties on the objects. For example, to change the title of an
item, just set the title property and youre done. To change the parent of an
item foo , just set the parent property to a new value bar , and everything gets
updated: bar now has foo in its children , and because we use fetched results
controllers the user interface also updates automatically.

Reordering
Reordering cells is also not possible in the sample application, but is mostly
straightforward to implement. Yet, there is one caveat: if you allow user-driven
reordering, you will update the order property in the model, and then get a
delegate call from the fetched results controller (which you should ignore,
because the cells have already moved). This is explained in the
NSFetchedResultsControllerDelegate documentation

Saving
Saving is as easy as calling save on the managed object context. Because we
dont access that directly, we do it in the store. The only hard part is when to save.
Apples sample code does it in applicationWillTerminate: , but depending on
your use case it could also be in applicationDidEnterBackground: or even
while your app is running.

Discussion
In writing this article and the example application, I made an initial mistake: I
chose to not have an empty root item, but instead let all the user-created items at
root level have a nil parent. This caused a lot of trouble: because the parent
item in the view controller could be nil , we needed to pass the store (or the
managed object context) around to each child view controller. Also, enforcing the
order invariant was harder, as we needed a fetch request to find an items
siblings, thus forcing Core Data to go back to disk. Unfortunately, these problems
were not immediately clear when writing the code, and some only became clear
when writing the tests. When rewriting the code, I was able to move almost all
code from the Store class into the Item class, and everything became a lot
cleaner.

http://www.objc.io/issues/4-core-data/full-core-data-application/

9/11

S-ar putea să vă placă și