A Complete Core Data Application Objc - Io PDF
A Complete Core Data Application Objc - Io PDF
io
By Chris Eidhof
In this article, we will build a small but complete Core Data backed application. In this article
The application allows you to create nested lists; each list item can have a sub-
list, allowing you to create very deep hierarchies of items. Instead of using the How Will We Build It?
Xcode template for Core Data, we will build our stack by hand, in order to fully
Set Up the Stack
understand whats going on. The example code for this application is on GitHub.
Creating a Model
First, we will create a PersistentStack object that, given a Core Data Model and Creating the Table Views Data Source
a filename, returns a managed object context. Then we will build our Core Data Creating the Table View Controller
Model. Next, we will create a simple table view controller that shows the root list
Adding Interactivity
of items using a fetched results controller, and add interaction step-by-step, by
Adding Items
adding items, navigating to sub-items, deleting items, and adding undo support.
Listening to Changes
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).
- (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;
}
http://www.objc.io/issues/4-core-data/full-core-data-application/ 2/11
6/29/2015 A Complete Core Data Application objc.io
+ (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;
}
- (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:
- (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];
}
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
- (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):
- (void)setFetchedResultsController:(NSFetchedResultsController*)fetchedResultsController
{
_fetchedResultsController = fetchedResultsController;
fetchedResultsController.delegate = self;
[fetchedResultsController performFetch:NULL];
}
- (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:
- (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;
}
http://www.objc.io/issues/4-core-data/full-core-data-application/ 4/11
6/29/2015 A Complete Core Data Application objc.io
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:
- (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 :
[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:
- (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:
- (void)controllerWillChangeContent:(NSFetchedResultsController*)controller
{
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController*)controller
{
[self.tableView endUpdates];
}
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:
- (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:
- (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:
- (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:
- (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.
- (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:
...
else if (type == NSFetchedResultsChangeDelete) {
[self.tableView deleteRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
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:
- (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
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 Blog Newsletter About Search
OBJECTIVE-C SELECT ALL
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