Attending to Data Quality - Pro iOS Persistence: Using Core Data (2014)

Pro iOS Persistence: Using Core Data (2014)

Chapter 4. Attending to Data Quality

If you have been diligently following the previous chapters, you should already be reasonably well versed in using the basics of Core Data. Dealing with errors, whether system errors or user errors, and seeding data are issues you must deal with when writing high-quality apps. This chapter is designed to help you fend off the real-world problems that arise when you apply all the theory to the real world of app development. We continue on with the BookStore application from Chapter 2.

Seeding Data

A question we hear often concerns how to distribute a persistent store that already contains some data. Many applications, for example, include a list of values for users to pick from to categorize the data they enter, and developers need to populate the data store with those values. You can approach this problem in several ways, including creating a SQLite database yourself and distributing that with your application instead of allowing Core Data to create the database itself, but this approach has its drawbacks, especially if a future version of your application augments this list of values. You really shouldn’t overwrite the user’s database file with a new, otherwise blank one that has more values in its list.

Another popular option is to use multiple persistent stores. One is used to contain the user’s data while the other is a static data store containing the seed values only. This solution works, but only if your app does not allow the user to augment the seeded lists. For example, if we seeded the BookStore app with a static list of categories in a separate store, we could never allow users to add their own, which would be very limiting.

If you’ve been following along with the examples in this book, you’ve already preloaded data into your data stores many times. Think about the applications in this book that don’t have edit user interfaces and how you got data into those. That’s right: you created managed objects in code and then saved the managed object context. Unfortunately, this solution also has problems of its own. For instance, if the seed data set is enormous, it will take forever for the app to insert all the records one at a time, horribly degrading the user experience. If you’ve tried the WordList application from Chapter 3, you probably noticed that inserting the nearly 170k words in the list took a few seconds from the time the list was downloaded to the time it was fully inserted into the store.

From experience, we find that the optimum option for seeding data lies somewhere between showing up with a fully loaded backing store and manually inserting rows. The remainder of this section expands on this idea by employing an initial seed that can be used if this is the first time ever that the user runs the app. When the app is updated, that list can be modified incrementally by hand because the bulk of the data will already be there.

In the BookStore application from Chapter 2, we seeded the data store by hard-coding a list of categories and books. We will use the seeded data store as our static seed. Go back to Chapter 2 (or download the source code—you can find the code samples for this chapter in the Source Code/Download area of the Apress web site [www.apress.com]) and run the app one last time, just to make sure we have created a seeded store.

Open the Terminal app on your Mac and find the BookStore.sqlite file (after you have run the app from Chapter 2 at least once). Once you find it, copy the file to your desktop and rename it seed.sqlite.

If you want to use the fast track, you can usually do it using this command from the Terminal prompt (all on one line):

find ~ -name BookStore.sqlite -exec cp {} ~/Desktop/seed.sqlite \;

Your desktop should now have a file called seed.sqlite, which contains all the seed data. One of the cardinal rules of software development is “Trust you did everything right, but verify anyway.” From the terminal, run sqlite3 ~/Desktop/seed.sqlite.

From the SQLite prompt, run

sqlite> select * from ZBOOKCATEGORY;

and you should see the data

Z_PK Z_ENT Z_OPT ZNAME
---------- ---------- ---------- ----------
3 2 1 Fiction
4 2 1 Biography

Again, if you run

sqlite> select * from ZBOOK;

the book data should be present.

Z_PK Z_ENT Z_OPT ZCATEGORY ZPRICE ZTITLE
------ ------ ------ ---------- --------- --------------
4 1 1 4 10.0 The third book
5 1 1 3 15.0 The second boo
6 1 1 3 10.0 The first book

Use the .quit command to exit the SQLite prompt.

At this point, we have our original seed data. Make a copy of the BookStore Xcode project from Chapter 2. From this point on, we will work on that copy.

Note It is important to make a copy so that when we launch the app, it is launched for the first time and therefore gets seeded. To be sure, go to the iOS simulator and remove the BookStore app from the simulated device (press and hold on the icon as on the real device).

Open the new BookStore project in Xcode.

Important Do not run the new app until we’re ready, or else it’ll create a data store before we’ve implemented the seeding procedure.

Let’s start with unhooking the manual seeding we created as part of Chapter 2. Open AppDelegate.m or AppDelegate.swift and go to the persistentStoreCoordinator method. For Objective-C, change the following line:

NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"BookStore.sqlite"];

to

NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"BookStoreEnhanced.sqlite"];

For Swift, change this line:

let storeURL = AppDelegate.applicationDocumentsDirectory.URLByAppendingPathComponent("BookStoreSwift.sqlite")

to

let storeURL = AppDelegate.applicationDocumentsDirectory.URLByAppendingPathComponent("BookStoreEnhancedSwift.sqlite")

This will change the name of the SQLite file used to back Core Data and make it easier for us to identify later in this chapter.

The next step, still in AppDelegate.m or AppDelegate.swift, is to blank out the initStore method, but keep the method around. We’ll put our new code in there later.

While you’re there, delete the deleteAllObjects method.

Finally, update the showExampleData method to simply list all the books in the store, as shown in Listing 4-1 (Objective-C) and Listing 4-2 (Swift).

Listing 4-1. Showing the Books in the Store (Objective-C)

- (void)showExampleData {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Book"];
NSArray *books = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];
for (Book *book in books) {
NSLog(@"Title: %@, price: %.2f", book.title, book.price);
}
}

Listing 4-2. Showing the Books in the Store (Swift)

func showExampleData() {
let fetchRequest = NSFetchRequest(entityName: "Book")
let books = self.managedObjectContext?.executeFetchRequest(fetchRequest, error: nil)
for book in books as [Book] {
println(String(format: "Title: \(book.title), price: %.2f", book.price))
}
}

Using the Seed Store

The first thing the app does when it starts, before executing any Core Data operations, is call the initStore method. This is exactly what we want because it’ll allow us to place our seed before Core Data initializes.

Let’s add the seed.sqlite file to the project. From your Desktop folder drag the seed.sqlite file onto the Xcode project navigator. Typically, you want to add the file to the Supporting Files group to keep your project organized.

When you drop the file into the Supporting Files group, Xcode gives you some options. Make sure you check Copy items into destination group's folder. In the Add to targets section, make sure BookStore is checked as well.

Your folder should look something like Figure 4-1 (Objective-C) or Figure 4-2 (Swift).

image

Figure 4-1. Adding the seed.sqlite file to BookStore (Objective-C)

image

Figure 4-2. Adding the seed.sqlite file to BookStore (Swift)

Now we’re ready to create the initial seed. Change the implementation of the initStore method as shown in Listing 4-3 (Objective-C) or Listing 4-4 (Swift).

Listing 4-3. Initial Seeding in Objective-C

- (void)initStore {
NSFileManager *fm = [NSFileManager defaultManager];

NSString *seed = [[NSBundle mainBundle] pathForResource: @"seed" ofType: @"sqlite"];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"BookStoreEnhanced.sqlite"];
if (![fm fileExistsAtPath:[storeURL path]]) {
NSLog(@"Using the original seed");
NSError *error = nil;
if (![fm copyItemAtPath:seed toPath:[storeURL path] error:&error]) {
NSLog(@"Error seeding: %@", error);
return;
}

NSLog(@"Store successfully initialized using the original seed");
}
else {
NSLog(@"The original seed isn't needed. There is already a backing store.");
}
}

Listing 4-4. Initial Seeding in Swift

func initStore() {
let fm = NSFileManager.defaultManager()

let seed = NSBundle.mainBundle().pathForResource("seed", ofType: "sqlite")
if let seed = seed {
let storeURL = AppDelegate.applicationDocumentsDirectory.URLByAppendingPathComponent("BookStoreEnhancedSwift.sqlite")

if !fm.fileExistsAtPath(storeURL.path!) {
println("Using the original seed")
var error: NSError? = nil
if !fm.copyItemAtPath(seed, toPath: storeURL.path!, error: &error) {
println("Seeding error: \(error?.localizedDescription)")
return
}
println("Store successfully initialized using the original seed.")
}
else {
println("The seed isn't needed. There is already a backing store.")
}
}
else {
println("Could not find the seed.")
}
}

Now you can launch the app. You’ll notice that even though we’ve removed all the hard-coded initialization code, your app will have data from the seed.

The output should look something like the following:

Using the original seed
Store successfully initialized using the original seed
Book fetched: The second book
Title: The second book, price: 15.00
Book fetched: The third book
Title: The third book, price: 10.00
Book fetched: The first book
Title: The first book, price: 10.00

If you quit and launch it again, you should get the following:

The original seed isn't needed. There is already a backing store.
Book fetched: The second book
Title: The second book, price: 15.00
Book fetched: The third book
Title: The third book, price: 10.00
Book fetched: The first book
Title: The first book, price: 10.00

We are now at the point were we can create an original seed for your app and it populates the seed in a flash, even if you have hundreds of thousands of rows, because it does it by copying the whole file at once instead of manually inserting each row individually.

If your seed data never change, you’re done! If on the other hand you want to update the seed in the future, keep on reading.

Updating Seeded Data in Subsequent Releases

Let’s assume we want to update our seed data with a fourth book. Whether users have been using your app for a while or they are getting introduced to the app directly from the latest version, they all need to be able to get that fourth book.

There are two parts to keeping everything in working order. First, you need to update your seed.sqlite so that it contains all the seed data. Second, you need to make sure existing users get the additional data.

Updating the Seed Data Store

Let’s start with updating the seed. This is the easy part. In order to achieve this, all we have to do is add the new book, with code, into the current data store, and then use that as the new seed.

Open AppDelegate.m or AppDelegate.swift and at the end of the initStore method, after all the seeding work, add the code in Listing 4-5 (Objective-C) or Listing 4-6 (Swift) to add the fourth book to one of the categories.

Listing 4-5. Adding a Fourth Book to the Seed (Objective-C)

- (void)initStore {
...

NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"BookCategory"];
NSArray *categories = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];
Category *category = [categories lastObject];
Book *book4 = [NSEntityDescription insertNewObjectForEntityForName:@"Book" inManagedObjectContext:self.managedObjectContext];
book4.title = @"The fourth book";
book4.price = 12;

[category addBooks:[NSSet setWithObjects:book4, nil]];

[self saveContext];
}

Listing 4-6. Adding a Fourth Book to the Seed (Swift)

func initStore() {
...

let fetchRequest = NSFetchRequest(entityName: "BookCategory")
let categories = self.managedObjectContext?.executeFetchRequest(fetchRequest, error: nil)

let category = categories?.last as BookCategory
var book4 = NSEntityDescription.insertNewObjectForEntityForName("Book", inManagedObjectContext: self.managedObjectContext!) as Book
book4.title = "The fourth book"
book4.price = 12

var booksRelation = category.valueForKeyPath("books") as NSMutableSet
booksRelation.addObject(book4)
saveContext()
}

Launch the app (only once or else it’ll keep adding) and the output should show that the fourth book has been added.

The original seed isn't needed. There is already a backing store.
New book created
Book fetched: The second book
Title: The second book, price: 15.00
Book fetched: The third book
Title: The third book, price: 10.00
Book fetched: The first book
Title: The first book, price: 10.00
Title: The fourth book, price: 12.00

At this point, we need to collect the data store and make that the seed. Let’s start with putting it on the Desktop by running the following at the Terminal prompt:

find ~ -name BookStoreEnhanced.sqlite -exec cp {} ~/Desktop/seed.sqlite \;

Check that the seed looks right by running the following command from the Terminal prompt:

sqlite3 ~/Desktop/seed.sqlite "select ZTITLE from ZBOOK";

You should see the new book in there.

ZTITLE
---------------
The second book
The third book
The first book
The fourth book

Okay, now that we’ve made a new seed, let’s go back to clean up our BookStore app. First remove the code you just added to initStore so that the fourth book no longer gets added. The initStore method should revert to the way it was shown in Listing 4-3 or Listing 4-4.

Since we don’t want to cheat, we need to reset the app so it forgets all about the fourth book and gets re-seeded from the old seed. In the iOS simulator, press and hold the BookStore icon and delete the app. Then launch it again. It should re-seed with our old seed and show only three books.

Using the original seed
Store successfully initialized using the original seed
Book fetched: The second book
Title: The second book, price: 15.00
Book fetched: The third book
Title: The third book, price: 10.00
Book fetched: The first book
Title: The first book, price: 10.00

Now let’s put the new seed in place. In Xcode, right-click on the seed.sqlite file and select “Show in Finder.” Using Finder, simply drag and drop the new seed we just made in the Desktop folder onto the old seed. Finder will ask you how you want to manage the file conflict. Just pick “Replace.” That’s it: the new seed is in place.

At this point, people who download your app for the first time will get the fourth book. Those who would get it as an update would not see the new book since the seed would not be used. We have to take care of these users.

Updating Existing Users

There are several options for updating data from the seed. The one you should choose depends largely on the requirements of your app. For example, sometimes it is possible to just dump the data store and replace it with the seed. This is the case if the user does not store anything in that store.

If you must update the store while preserving the users’ data, you should consider manually updating it so that updates are incremental.

Let’s edit the initStore method once again to support incremental updates. We want to pick up the new records we want to add (in this case, there’s only one—the fourth book) and if they’re not already there, then apply all the updates that were released at the same time. Then repeat that block for each new version of the app that you create. Listing 4-7 (Objective-C) and Listing 4-8 (Swift) shows the initStore method that manages incremental updates.

Listing 4-7. Initial Seeding and Incremental Updates (Objective-C)

- (void)initStore {
NSFileManager *fm = [NSFileManager defaultManager];

NSString *seed = [[NSBundle mainBundle] pathForResource: @"seed" ofType: @"sqlite"];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"BookStoreEnhanced.sqlite"];
if(![fm fileExistsAtPath:[storeURL path]]) {
NSLog(@"Using the original seed");
NSError * error = nil;
if (![fm copyItemAtPath:seed toPath:[storeURL path] error:&error]) {
NSLog(@"Error seeding: %@",error);
return;
}

NSLog(@"Store successfully initialized using the original seed");
}
else {
NSLog(@"The original seed isn't needed. There is already a backing store.");

// Update 1
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Book"];
request.predicate = [NSPredicate predicateWithFormat:@"title=%@", @"The fourth book"];
NSUInteger count = [self.managedObjectContext countForFetchRequest:request error:nil];
if(count == 0) {
NSLog(@"Applying batch update 1");

NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"BookCategory"];
NSArray *categories = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];

BookCategory *category = [categories lastObject];
Book *book4 = [NSEntityDescription insertNewObjectForEntityForName:@"Book" inManagedObjectContext:self.managedObjectContext];
book4.title = @"The fourth book";
book4.price = 12;

[category addBooks:[NSSet setWithObjects:book4, nil]];

[self.managedObjectContext save:nil];

NSLog(@"Update 1 successfully applied");
}
} }
}

Listing 4-8. Initial Seeding and Incremental Updates (Swift)

func initStore() {
let fm = NSFileManager.defaultManager()

let seed = NSBundle.mainBundle().pathForResource("seed", ofType: "sqlite")
if let seed = seed {
let storeURL = AppDelegate.applicationDocumentsDirectory.URLByAppendingPathComponent("BookStoreEnhancedSwift.sqlite")

if !fm.fileExistsAtPath(storeURL.path!) {
println("Using the original seed")
var error: NSError? = nil
if !fm.copyItemAtPath(seed, toPath: storeURL.path!, error: &error) {
println("Seeding error: \(error?.localizedDescription)")
return
}
println("Store successfully initialized using the original seed.")
}
else {
println("The seed isn't needed. There is already a backing store.")

// Update 1
if let managedObjectContext = self.managedObjectContext {
let fetchRequest1 = NSFetchRequest(entityName: "Book")
fetchRequest1.predicate = NSPredicate(format: "title=%@", argumentArray: ["The fourth book"])
if managedObjectContext.countForFetchRequest(fetchRequest1, error: nil) == 0 {
println("Applying batch update 1")
let fetchRequest = NSFetchRequest(entityName: "BookCategory")
let categories = managedObjectContext.executeFetchRequest(fetchRequest, error: nil)

let category = categories?.last as BookCategory
var book4 = NSEntityDescription.insertNewObjectForEntityForName("Book", inManagedObjectContext: managedObjectContext) as Book
book4.title = "The fourth book"
book4.price = 12

var booksRelation = category.valueForKeyPath("books") as NSMutableSet
booksRelation.addObject(book4)

saveContext()
println("Update 1 successfully applied")
}
}
}
}
else {
println("Could not find the seed.")
}
}

You can finally run the app and since you had launched the first version already, it will not re-seed but instead will update the app incrementally.

The original seed isn't needed. There is already a backing store.
Applying batch update 1
New book created
Update 1 successfully applied
Book fetched: The second book
Title: The second book, price: 15.00
Book fetched: The third book
Title: The third book, price: 10.00
Book fetched: The first book
Title: The first book, price: 10.00
Title: The fourth book, price: 12.00

And if you run it again, it doesn’t apply any new seed or update.

The original seed isn't needed. There is already a backing store.
Book fetched: The second book
Title: The second book, price: 15.00
Book fetched: The third book
Title: The third book, price: 10.00
Book fetched: The first book
Title: The first book, price: 10.00
Title: The fourth book, price: 12.00

The last test scenario is to delete your app from the simulator once again and launch it to simulate what a new user will get.

Using the original seed
Store successfully initialized using the original seed
Book fetched: The second book
Title: The second book, price: 15.00
Book fetched: The third book
Title: The third book, price: 10.00
Book fetched: The first book
Title: The first book, price: 10.00
Title: The fourth book, price: 12.00

It simply uses the seed and does not try to apply any updates. In all cases, the complete set of seed data is available.

Undoing and Redoing

Golfers call it a mulligan. Schoolyard children call it a do-over. Computer users call it Edit and Undo. Whatever you call it, you’ve realized that you’ve blundered and want to take back your last action. Not all scenarios afford you that opportunity, to which many broken-hearted lovers will attest, but Core Data forgives and allows you to undo what you’ve done using the standard Cocoa NSUndoManager mechanism. This section instructs you how to use it to allow your users to undo their Core Data changes.

The Core Data undo manager, an object of type NSUndoManager, lives in your managed object context, and NSManagedObjectContext provides a getter and a setter for the undo manager. Unlike Core Data on Mac OS X, however, the managed object context in iOS’s Core Data doesn’t provide an undo manager by default for performance reasons. If you want to undo capabilities for your Core Data objects, you must set the undo manager in your managed object context yourself.

If you want to support undoing actions in your iOS application, you typically create the undo manager when you set up your managed object context, which usually happens in the getter for the managed object context. The BookStore application, for example, sets up the managed object context in the application delegate, as shown in Listing 4-9 (Objective-C) or Listing 4-10 (Swift). Simply set the undo manager there and you are all set.

Listing 4-9. Setting up the Undo Manager (Objective-C)

- (NSManagedObjectContext *)managedObjectContext {
if (_managedObjectContext != nil) {
return _managedObjectContext;
}

NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];

// Add the undo manager
[_managedObjectContext setUndoManager:[[NSUndoManager alloc] init]];

return _managedObjectContext;
}

Listing 4-10. Setting up the Undo Manager (Swift)

lazy var managedObjectContext: NSManagedObjectContext? = {
// Initialize the managed object model
let modelURL = NSBundle.mainBundle().URLForResource("BookStoreSwift", withExtension: "momd")
let managedObjectModel = NSManagedObjectModel(contentsOfURL: modelURL!)

// Initialize the persistent store coordinator
let storeURL = AppDelegate.applicationDocumentsDirectory.URLByAppendingPathComponent("BookStoreEnhancedSwift.sqlite")
var error: NSError? = nil
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel!)

if(persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil, error: &error) == nil) {
self.showCoreDataError()
return nil
}

var managedObjectContext = NSManagedObjectContext()
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator

// Add the undo manager
managedObjectContext.undoManager = NSUndoManager()

return managedObjectContext
}()

Once the undo manager is set into the managed object context, it tracks any changes in the managed object context and adds them to the undo stack. You can undo those changes by calling the undo method of NSUndoManager, and each change (actually, each undo group, as explained in the section “Undo Groups”) is rolled back from the managed object context. You can also replay changes that have been undone by calling NSUndoManager’s redo method.

The undo and redo methods perform their magic only if the managed object context has any change to undo or redo, so calling them when no changes can be undone or redone does nothing. You can check, however, if the undo manager can undo or redo any changes by calling thecanUndo and canRedo methods, respectively.

Undo Groups

By default, the undo manager groups all changes that happen during a single pass through the application’s run loop into a single change that can be undone or redone as a unit. This means, for example, that in the WordList application of Chapter 3, all word creations belong to the same group because they are all created during a for-loop without releasing control of the thread until done.

You can alter this behavior by turning off automatic grouping completely and managing the undo groups yourself. To accomplish this, pass NO to setGroupsByEvent. You become responsible, then, for creating all undo groups, because the undo manager will no longer create them for you. You create the undo group by calling beginUndoGrouping to start creating the group and endUndoGrouping to complete the undo group. These calls must be matched, or an exception of type NSInternalInconsistencyException is raised. You could, for example, create an undo group for each word creation in WordList so that you can undo the creation one word at a time. The grouping strategy depends largely on the specific requirements of your application.

Limiting the Undo Stack

By default, the undo manager tracks an unlimited number of changes for you to undo and redo. This can cause memory issues, especially on iOS devices. You can limit the size of the undo stack by calling NSUndoManager’s setLevelsOfUndo: method, passing an unsigned integer that represents the number of undo groups to retain on the undo stack. You can inspect the current undo stack size, measured in the number of undo groups, by calling levelsOfUndo, which returns an unsigned integer. A value of 0 represents no limit. If you’ve imposed a limit on the size of the undo stack, the oldest undo groups roll off the stack to accommodate the newer groups.

Disabling Undo Tracking

Once you create an undo manager and set it into the managed object context, any changes you make to the managed object context are tracked and can be undone. You can disable undo tracking, however, by calling NSUndoManager’s disableUndoRegistration method. To re-enable undo tracking, call NSUndoManager’s enableUndoRegistration method. Disabling and enabling undo tracking use a reference counting mechanism, so multiple calls to disableUndoRegistration require an equal number of calls to enableUndoRegistrationbefore undo tracking becomes enabled again.

Calling enableUndoRegistration when undo tracking is already enabled raises an exception of type NSInternalInconsistencyException, which will likely crash your application. To avoid this embarrassment, you can call NSUndoManager’sisUndoRegistrationEnabled, which returns a BOOL, before calling enableUndoRegistration. For example, the following code checks whether undo tracking is enabled before enabling it:

if (![undoManager isUndoRegistrationEnabled]) {
[undoManager enableUndoRegistration];
}

The isUndoRegistrationEnabled method disappeared in iOS 8, however, and doesn’t exist in Swift, so you’re responsible for keeping track of your diable/enable calls. You can clear the undo stack entirely by calling the removeAllActions method. This method has the side effect of re-enabling undo tracking.

Adding Undo to BookStore

Let’s put all this into practice with the BookStore app. We’ve already added the undo manager at the beginning of this section. Let’s create some code that’ll add a few books and we’ll undo the change at the end to show how it works.

Before we do anything else, since we’ll be playing around with inserting data, let’s force the app to use the original seed every time it launches. This will, of course, reset the app at every launch, so it would not be suitable for most production applications. In the context of our experiments, however, this is perfect because we will always start the app with a known data set, no matter how many changes we experiment with.

Update the initStore method to force it to use the seed, as shown in Listing 4-11 (Objective-C) or Listing 4-12 (Swift).

Listing 4-11. Forcing BookStore to Always Use the Seed (Objective-C)

- (void)initStore {
NSFileManager *fm = [NSFileManager defaultManager];

NSString *seed = [[NSBundle mainBundle] pathForResource: @"seed" ofType: @"sqlite"];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"BookStoreEnhanced.sqlite"];

if([fm fileExistsAtPath:[storeURL path]]) {
[fm removeItemAtPath:[storeURL path] error:nil];
}

if(![fm fileExistsAtPath:[storeURL path]]) {
NSLog(@"Using the original seed");
...

Listing 4-12. Forcing BookStore to Always Use the Seed (Swift)

func initStore() {
let fm = NSFileManager.defaultManager()

let seed = NSBundle.mainBundle().pathForResource("seed", ofType: "sqlite")
if let seed = seed {
let storeURL = AppDelegate.applicationDocumentsDirectory.URLByAppendingPathComponent("BookStoreEnhancedSwift.sqlite")

if fm.fileExistsAtPath(storeURL.path!) {
fm.removeItemAtPath(storeURL.path!, error: nil)
}

if !fm.fileExistsAtPath(storeURL.path!) {
println("Using the original seed")
...

This will do it—it will always delete the existing data store if it exists.

In AppDelegate.m or AppDelegate.swift, create a new method called insertSomeData and add the code shown in Listing 4-13 (Objective-C) or Listing 4-14 (Swift).

Listing 4-13. Setting up Data to Undo (Objective-C)

- (void)insertSomeData {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"BookCategory"];
NSArray *categories = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];

BookCategory *category = [categories lastObject];

for(int i=5; i<10; i++) {
Book *book = [NSEntityDescription insertNewObjectForEntityForName:@"Book" inManagedObjectContext:self.managedObjectContext];
book.title = [NSString stringWithFormat:@"The %dth book", i];
book.price = i;
[category addBooksObject:book];
}

[self saveContext];
}

Listing 4-14. Setting up Data to Undo (Objective-C)

func insertSomeData() {
let category = self.managedObjectContext?.executeFetchRequest(NSFetchRequest(entityName: "BookCategory"), error: nil)?.last as BookCategory?

if let managedObjectContext = self.managedObjectContext {
managedObjectContext.undoManager?.groupsByEvent = false

if let category = category {
for i in 5..<10 {
var book = NSEntityDescription.insertNewObjectForEntityForName("Book", inManagedObjectContext: managedObjectContext) as Book
book.title = "The \(i)th book"
book.price = Float(i)

var booksRelation = category.valueForKeyPath("books") as NSMutableSet
booksRelation.addObject(book)
}
saveContext()
}
}
}

Use the new method by going to the application:didFinishLaunchingWithOptions: method and calling it right after initStore and right before showExampleData, as shown in Listing 4-15 (Objective-C) or Listing 4-16 (Swift).

Listing 4-15. Calling insertSomeData in Objective-C

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self initStore];
[self insertSomeData];
[self showExampleData];
return YES;
}

Listing 4-16. Calling insertSomeData in Swift

func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
initStore()
insertSomeData()
showExampleData()
return true
}

If you launch the app, you will see the five new books created (in addition to those from the seed).

Using the original seed
Store successfully initialized using the original seed
New book created
New book created
New book created
New book created
New book created
Book fetched: The second book
Title: The second book, price: 15.00
Book fetched: The third book
Title: The third book, price: 10.00
Book fetched: The first book
Title: The first book, price: 10.00
Book fetched: The fourth book
Title: The fourth book, price: 12.00
Title: The 9th book, price: 9.00
Title: The 5th book, price: 5.00
Title: The 6th book, price: 6.00
Title: The 8th book, price: 8.00
Title: The 7th book, price: 7.00

Now let’s utilize the undo manager by calling undo at the end of the insertSomeData method, as shown in Listing 4-17 (Objective-C) or Listing 4-18 (Swift). Since all of the inserts were made during the same iteration of the run loop, one call to undo should undo all five books.

Listing 4-17. Testing the Undo Manager (Objective-C)

- (void)insertSomeData {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"BookCategory"];
NSArray *categories = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];

BookCategory *category = [categories lastObject];

for(int i=5; i<10; i++) {
Book *book = [NSEntityDescription insertNewObjectForEntityForName:@"Book" inManagedObjectContext:self.managedObjectContext];
book.title = [NSString stringWithFormat:@"The %dth book", i];
book.price = i;
[category addBooksObject:book];
}

[self.managedObjectContext.undoManager undo];
[self saveContext];
}

Listing 4-18. Testing the Undo Manager (Swift)

func insertSomeData() {
let category = self.managedObjectContext?.executeFetchRequest(NSFetchRequest(entityName: "BookCategory"), error: nil)?.last as BookCategory?

if let managedObjectContext = self.managedObjectContext {
if let category = category {
for i in 5..<10 {
var book = NSEntityDescription.insertNewObjectForEntityForName("Book", inManagedObjectContext: managedObjectContext) as Book
book.title = "The \(i)th book"
book.price = Float(i)

var booksRelation = category.valueForKeyPath("books") as NSMutableSet
booksRelation.addObject(book)
}

managedObjectContext.undoManager?.undo()
saveContext()
}
}
}

Run the method once again and notice that even though all the books were created, they are not in the store when we display the data since the change has been undone.

Using the original seed
Store successfully initialized using the original seed
New book created
New book created
New book created
New book created
New book created
Book fetched: The second book
Title: The second book, price: 15.00
Book fetched: The third book
Title: The third book, price: 10.00
Book fetched: The first book
Title: The first book, price: 10.00
Book fetched: The fourth book
Title: The fourth book, price: 12.00

If you add a call to redo right after the call to undo, as shown next, what was undone will be redone and the new books will be in the store when you display the data.

[self.managedObjectContext.undoManager redo]; // Objective-C
managedObjectContext.undoManager?.redo() // Swift

Experimenting with the Undo Groups

In this section, we modify the data insert method to put each new book in its own undo group. The effect of that, of course, is that when the undo method is called, only one insert will be undone per call. Listing 4-19 (Objective-C) and Listing 4-20 (Swift) shows the updated code.

Listing 4-19. Managing Our Own Undo Groups (Objective-C)

- (void)insertSomeData {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"BookCategory"];
NSArray *categories = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];

BookCategory *category = [categories lastObject];

// Tell the undo manager that from now on, we manage grouping
[self.managedObjectContext.undoManager setGroupsByEvent:NO];

for(int i=5; i<10; i++) {
// Start a new group
[self.managedObjectContext.undoManager beginUndoGrouping];

Book *book = [NSEntityDescription insertNewObjectForEntityForName:@"Book" inManagedObjectContext:self.managedObjectContext];
book.title = [NSString stringWithFormat:@"The %dth book", i];
book.price = i;
[category addBooksObject:book];

// End the current group
[self.managedObjectContext.undoManager endUndoGrouping];
}

[self.managedObjectContext.undoManager undo];
[self saveContext];
}

Listing 4-20. Managing Our Own Undo Groups (Swift)

func insertSomeData() {
let category = self.managedObjectContext?.executeFetchRequest(NSFetchRequest(entityName: "BookCategory"), error: nil)?.last as BookCategory?

if let managedObjectContext = self.managedObjectContext {
// Tell the undo manager that from now on, we manage grouping
managedObjectContext.undoManager?.groupsByEvent = false

if let category = category {
for i in 5..<10 {
// Start a new group
managedObjectContext.undoManager?.beginUndoGrouping()

var book = NSEntityDescription.insertNewObjectForEntityForName("Book", inManagedObjectContext: managedObjectContext) as Book
book.title = "The \(i)th book"
book.price = Float(i)

var booksRelation = category.valueForKeyPath("books") as NSMutableSet
booksRelation.addObject(book)

// End the current group
managedObjectContext.undoManager?.endUndoGrouping()
}

managedObjectContext.undoManager?.undo()
saveContext()
}
}
}

Launch the app and since there is a call to undo, the ninth book is missing. Add a second call to undo in that method and notice how both the eighth and ninth books are missing.

Dealing with Errors

When you ask Xcode to generate a Core Data application for you, it creates boilerplate code for every vital aspect of talking to your Core Data persistent store. This code is more than adequate for setting up your persistent store coordinator, your managed object context, and your managed object model. In fact, if you go back to a non–Core Data project and add Core Data support, you’ll do well to drop in the same code, just as Xcode generates it, to manage Core Data interaction. The code is production-ready.

That is, it is production-ready except in one aspect: error handling.

The Xcode-generated code alerts you to this shortcoming with a comment that says the following:

// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

The Xcode-generated implementation logs the error and aborts the application, which is a decidedly unfriendly approach. All users see when this happens is your application abruptly disappearing, without any clue as to why. If this happens in a live application, you’ll get poor reviews and low sales.

Happily, however, you don’t have to fall into this trap of logging and crashing. In this section, we explore strategies for handling errors in Core Data. No one strategy is the best, but this section should spark ideas for your specific applications and audiences and help you devise an error-handling strategy that makes sense.

Errors in Core Data can be divided into two major categories.

· Errors in normal Core Data operations

· Validation errors

The next sections discuss strategies for handling both types of errors.

Handling Core Data Operational Errors

All the examples in this book respond to any Core Data errors using the default, Xcode-generated error-handling code, dutifully outputting the error message to Xcode’s console and aborting the application. This approach has two advantages.

· It helps diagnose issues during development and debugging.

· It’s easy to implement.

These advantages help only you as developer, however, and do nothing good for the application’s users. Before you publicly release an application that uses Core Data, you should design and implement a better strategy for responding to errors. The good news is that the strategy needn’t be large or difficult to implement, because your options for how to respond are limited. Although applications are all different, in most cases you won’t be able to recover from a Core Data error and should probably display an alert and instruct the user to close the application. Doing this explains to users what happened and gives them control over when to terminate the app. It’s not much control, but hey—it’s better than just having the app disappear.

Virtually all Core Data operational errors should be caught during development, so careful testing of your app should prevent these scenarios.

Currently, the BookStore app has user interface, albeit blank. We currently load the Core Data stack when the application launches, before the user interface displays. If we wait for the user interface to load before initializing Core Data, however, we’ll have an easier time displaying an error message. Let’s change the application to do that.

If you’re working with Objective-C, open ViewController.m and add the imports and methods shown in Listing 4-21. If you’re working with Swift, open ViewController.swift and add the functions that Listing 4-22 shows.

Listing 4-21. Updating ViewController.m

#import "AppDelegate.h"
#import "BookCategory.h"
#import "Book.h"
#import "Page.h"

- (NSURL *)applicationDocumentsDirectory {
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}

- (NSManagedObjectContext*)managedObjectContext {
AppDelegate *ad = [UIApplication sharedApplication].delegate;
return ad.managedObjectContext;
}

- (void)saveContext {
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
NSError *error = nil;
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
}

Listing 4-22. Updating ViewController.swift

lazy var managedObjectContext: NSManagedObjectContext? = {
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
return appDelegate.managedObjectContext
}()

func saveContext() {
var error: NSError? = nil
if let managedObjectContext = self.managedObjectContext {
if managedObjectContext.hasChanges && !managedObjectContext.save(&error) {
let message = validationErrorText(error!)
println("Error: \(message)")
}
}
}

Next, move the initStore, showExampleData, and insertSomeData methods from AppDelegate.m to ViewController.m, or from AppDelegate.swift to ViewController.swift.

We will have the controller call those methods once it has displayed its view. Add the method shown in Listing 4-23 to ViewController.m, or the function in Listing 4-24 to ViewController.swift.

Listing 4-23. Overriding the viewDidAppear: Method

-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self initStore];
[self showExampleData];
}

Listing 4-24. Overriding the viewDidAppear function

override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if let managedObjectContext = self.managedObjectContext {
self.initStore()
self.showExampleData()
}
}

Remove the calls to initStore, insertSomeData, and showExample data from application:didFinishLaunchingWithOptions: in AppDelegate.m or AppDelegate.swift.

Run the app and everything should work exactly the way it did before, except this time we’re out of the startup sequence when we start interacting with Core Data.

To add error-handling code to the BookStore application, add a method to AppDelegate.m or AppDelegate.swift to dosplay the error. You can quibble with the wording, but remember that error messages aren’t a paean to the muses, and the longer the message, the less likely it will be read. Listing 4-25 (Objective-C) and Listing 4-26 (Swift) contain implementations with short and simple messages—with only an extraneous exclamation mark to plead with the user to read it.

Listing 4-25. A Method to Show a Core Data Error (Objective-C)

- (void)showCoreDataError {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error!" message:@"BookStore can't continue.\nPress the Home button to close the app." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alert show];
}

Listing 4-26. A Method to Show a Core Data Error (Swift)

func showCoreDataError() {
var alert = UIAlertController(title: "Error!", message: "BookStore can't continue.\nPress the Home button to close the app.", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
self.window?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}

Now, change the persistentStoreCoordinator accessor method to use this new method instead of logging and aborting. Listing 4-27 (Objective-C) and Listing 4-28 (Swift) show the updates to persistentStoreCoordinator.

Listing 4-27. The Updated persistentStoreCoordinator Method (Objective-C)

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}

_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"BookStoreEnhanced.sqlite"];
NSError *error = nil;
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
[self showCoreDataError];
}

return _persistentStoreCoordinator;
}

Listing 4-28. The Updated persistentStoreCoordinator creation in the managedObjectContext Function (Swift)

if(persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil, error: &error) == nil) {
self.showCoreDataError()
return nil
}

To force this error to display, run the BookStore application, and then close it. Go to the data model, add an attribute called foo of type String to the Book entity, and then run the application again. The persistent store coordinator will be unable to open the data store because the model no longer matches, and your new error message will display as Figure 4-3 shows.

image

Figure 4-3. Showing a Core Data Error condition

Of course, at this point your app should mark itself “unstable” so that it prevents further attempts to interact with Core Data, or else your users will find themselves dealing with a surge of alert boxes to dismiss. You may have noticed that the implementation of the viewDidAppear:method did not try to insert any new data. If it did, you would have to deal with catching more exceptions coming from the non-initialized persistent store. This is because we’ve never told the app that it was unstable so it keeps on trying to access the persistent store. Let’s deal with it.

Add the following property in AppDelegate.h:

@property (nonatomic) BOOL unstable;

Or in AppDelegate.swift:

var unstable: Bool?

We will set this property as needed in showCoreDataError, as shown in Listing 4-29 (Objective-C) or Listing 4-30 (Swift).

Listing 4-29. Marking the App as Unstable (Objective-C)

- (void)showCoreDataError {
self.unstable = YES;

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error!" message:@"BookStore can't continue.\nPress the Home button to close the app." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alert show];
}

Listing 4-30. Marking the App as Unstable (Swift)

func showCoreDataError() {
self.unstable = true

var alert = UIAlertController(title: "Error!", message: "BookStore can't continue.\nPress the Home button to close the app.", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
self.window?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}

In ViewController.m or ViewController.swift, in viewDidAppear:, add the call to insertSomeData.

Finally, we make sure we don’t cause extra damage by preventing inserts if the store isn’t correctly initialized by editing the insertSomeData method, as shown in Listing 4-31 Objective-C) or Listing 4-32 (Swift).

Listing 4-31. Preventing Inserts If the App Is Unstable (Objective-C)

- (void)insertSomeData {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"BookCategory"];
NSArray *categories = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];

AppDelegate *ad = [UIApplication sharedApplication].delegate;
if(ad.unstable) {
NSLog(@"The app is unstable. Preventing updates.");
return;
}
...

Listing 4-32. Preventing Inserts If the App Is Unstable (Swift)

func insertSomeData() {
let ad = UIApplication.sharedApplication().delegate as? AppDelegate

if let unstable = ad?.unstable {
if unstable {
println("The app is unstable. Preventing updates.")
return
}
}
...

Try launching the app. You should see the alert as well as the note in the log about preventing updates.

Don’t forget to undo the change you made to the data model so that we can put the app back into working order. Simply delete the foo attribute, then launch to make sure the app runs fine.

Handling Validation Errors

If you’ve configured any properties in your Core Data model with any validation parameters and you allow users to input values that don’t automatically meet those validation parameters, you can expect to have validation errors. Validations ensure the integrity of your data; Core Data won’t store anything you’ve proclaimed invalid into the persistent store. Just because you’ve created the validation rules, however, doesn’t mean that users are aware of them—or that they know that they can violate them. If you leave the Xcode-generated error handling in place, users won’t know they’ve violated the validation rules even after they input invalid data. All that will happen is that your application will crash, logging a long stack trace that the users will never see. Bewildered users will be left with a crashing application, and they won’t know why or how to prevent its occurrence. Instead of crashing when users enter invalid data, you should instead alert users and give them an opportunity to correct the data.

Validation on the database side can be a controversial topic, and for good reason. You can protect your data’s integrity at its source by putting your validation rules in the data model, but you’ve probably made your coding tasks more difficult. Validation rules in your data model are one of those things that sound good in concept but prove less desirable in practice. Can you imagine, for example, using Oracle to do field validation on a web application? Yes, you can do it, but other approaches are probably simpler, more user-friendly, and architecturally superior. Validating user-entered values in code, or even designing user interfaces that prevent invalid entry altogether, makes your job easier and users’ experiences better.

Having said that, however, we’ll go ahead and outline a possible strategy for handling validation errors. Don’t say we didn’t warn you, though.

Detecting that users have entered invalid data is simple: just inspect the NSError object that you pass to the managed object context’s save: method if an error occurs. The NSError object contains the error code that caused the save: method to fail, and if that code matches one of the Core Data validation error codes shown in Table 4-1, you know that some part of the data you attempted to save was invalid. You can use NSError’s userInfo dictionary to look up more information about what caused the error. Note that if multiple errors occurred, the error code is 1560,NSValidationMultipleErrorsError, and the userInfo dictionary holds the rest of the error codes in the key called NSDetailedErrorsKey.

Table 4-1. Core Data Validation Errors

Constant

Code

Description

NSManagedObjectValidationError

1550

Generic validation error

NSValidationMultipleErrorsError

1560

Generic message for error containing multiple validation errors

NSValidationMissingMandatoryPropertyError

1570

Non-optional property with a nil value

NSValidationRelationshipLacksMinimumCountError

1580

To-many relationship with too few destination objects

NSValidationRelationshipExceedsMaximumCountError

1590

Bounded, to-many relationship with too many destination objects

NSValidationRelationshipDeniedDeleteError

1600

Some relationship with NSDeleteRuleDeny is nonempty

NSValidationNumberTooLargeError

1610

Some numerical value is too large

NSValidationNumberTooSmallError

1620

Some numerical value is too small

NSValidationDateTooLateError

1630

Some date value is too late

NSValidationDateTooSoonError

1640

Some date value is too soon

NSValidationInvalidDateError

1650

Some date value fails to match date pattern

NSValidationStringTooLongError

1660

Some string value is too long

NSValidationStringTooShortError

1670

Some string value is too short

NSValidationStringPatternMatchingError

1680

Some string value fails to match some pattern

You can choose to implement an error-handling routine that’s familiar with your data model and thus checks only for certain errors, or you can write a generic error-handling routine that will handle any of the validation errors that occur. Though a generic routine scales better and should continue to work no matter the changes to your data model, a more specific error-handling routine may allow you to be more helpful to your users in your messaging and responses. Neither is the correct answer—the choice is yours for how you want to approach validation error handling.

To write a truly generic validation error-handling routine would be a lot of work. One thing to consider is that the NSError object contains a lot of information about the error that occurred, but not necessarily enough information to tell the user why the validation failed. Imagine, for example, that you have an entity Foo with an attribute bar that must be at least five characters long. If the user enters “abc” for bar, you’ll get an NSError message that tells you the error code (1670), the entity (Foo), the attribute (bar), and the value (abc) that failed validation. TheNSError object doesn’t tell you why abc is too short—it contains no information that bar requires at least five characters. To arrive at that, you’d have to ask the Foo entity for the NSPropertyDescription for the bar attribute, get the validation predicates for that property description, and walk through the predicates to see what the minimum length is for bar. It’s a noble goal but tedious and usually overkill. This is one place where violating “Don’t Repeat Yourself” (DRY) and letting your code know something about the data model might be a better answer.

One other strange thing to consider when using validations in your data model is that they aren’t enforced when you create a managed object; they’re enforced only when you try to save the managed object context that the managed object lives in. This makes sense if you think it through, since creating a managed object and populating its properties happens in multiple steps. First, you create the object in the context, and then you set its attributes and relationships. So, for example, if you were creating the managed object for the Foo entity in the previous paragraph, you’d write code as follows:

NSManagedObject *foo = [NSEntityDescription insertNewObjectForEntityForName:@"Foo" inManagedObjectContext:context]; // foo is invalid at this point; bar has fewer than five characters
[foo setValue:@"abcde" forKey:@"bar"];

The managed object foo is created and lives in the managed object context in an invalid state, but the managed object context ignores that. The next line of code makes the foo managed object valid, but it won’t be validated until the managed object context is saved.

Handling Validation Errors in BookStore

In this section, you implement a validation error-handling routine for the BookStore application. It’s generic in that it doesn’t have any knowledge of which attributes have validation rules, but it is specific in that it doesn’t handle all the validation errors—just the ones that you know you set on the model. Before doing that, however, you need to add some validation rules to BookStore’s data model. Add a minimum value of 15 to the price attribute of the Book entity as shown in Figure 4-4.

image

Figure 4-4. Adding a validation rule

Implementing the Validation Error-Handling Routine

The validation error-handling routine you write should accept a pointer to an NSError object and return an NSString that contains the error messages, separated by line feeds. Open ViewController.m or ViewController.swift, and add the method shown in Listing 4-33(Objective-C) or Listing 4-34 (Swift).

Listing 4-33. Adding a Validation Error-Handling Routine (Objective-C)

- (NSString *)validationErrorText:(NSError *)error {
// Create a string to hold all the error messages
NSMutableString *errorText = [NSMutableString stringWithCapacity:100];
// Determine whether we're dealing with a single error or multiples, and put them all
// in an array
NSArray *errors = [error code] == NSValidationMultipleErrorsError ? [[error userInfo] objectForKey:NSDetailedErrorsKey] : [NSArray arrayWithObject:error];

// Iterate through the errors
for (NSError *err in errors) {
// Get the property that had a validation error
NSString *propName = [[err userInfo] objectForKey:@"NSValidationErrorKey"];
NSString *message;

// Form an appropriate error message
switch ([err code]) {
case NSValidationNumberTooSmallError:
message = [NSString stringWithFormat:@"%@ must be at least $15", propName];
break;
default:
message = @"Unknown error. Press Home button to halt.";
break;
}

// Separate the error messages with line feeds
if ([errorText length] > 0) {
[errorText appendString:@"\n"];
}
[errorText appendString:message];
}
return errorText;
}

Listing 4-34. Adding a Validation Error-Handling Routine (Swift)

func validationErrorText(error : NSError) -> String {
// Create a string to hold all the error messages
let errorText = NSMutableString(capacity: 100)
// Determine whether we're dealing with a single error or multiples, and put them all
// in an array
let errors : NSArray = error.code == NSValidationMultipleErrorsError ? error.userInfo?[NSDetailedErrorsKey] as NSArray : NSArray(object: error)

// Iterate through the errors
for err in errors {
// Get the property that had a validation error
let e = err as NSError
let info = e.userInfo
let propName : AnyObject? = info!["NSValidationErrorKey"]
var message : String?

// Form an appropriate error message
switch err.code {
case NSValidationNumberTooSmallError:
message = "\(propName!) must be at least $15"
default:
message = "Unknown error. Press Home button to halt."
}

// Separate the error messages with line feeds
if errorText.length > 0 {
errorText.appendString("\n")
}

errorText.appendString(message!)
}

return errorText
}

We now have a method that returns a more useful message to help the user. Let’s hook it up to our test code. Since some of the books we insert in the insertSomeData method have a price lower than $15, the error should kick in.

Edit the saveContext method in the view controller to call the validationErrorText method, as Listing 4-35 (Objective-C) and Listing 4-36 (Swift) show.

Listing 4-35. Calling the Validation Error-Handling Routine (Objective-C)

- (void)saveContext {
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
NSError *error = nil;
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
NSString *message = [self validationErrorText:error];
NSLog(@"Error: %@", message);
}
}
}

Listing 4-36. Calling the Validation Error-Handling Routine (Swift)

func saveContext() {
var error: NSError? = nil
if let managedObjectContext = self.managedObjectContext {
if managedObjectContext.hasChanges && !managedObjectContext.save(&error) {
let message = validationErrorText(error!)
println("Error: \(message)")
}
}
}

Launch the BookStore app and you will see the validation errors in the logs where the books we tried to insert were invalid.

Summary

In this chapter, you have seen several techniques to help you manage the quality and the integrity of the data. You’ve learned how to help your users deal with unexpected failures and how to help manage a smooth landing instead of letting your application crash inexplicably. In the next chapter, you will learn how to delight your users by integrating the user interface with Core Data, how to smoothly migrate your data when upgrading your app, and many more advanced features all contributing to the quality of your applications.