Chapter 11

Core Data Recipes

One of the most common problems that developers face is the concept of persistence, or more simply, storing data. For certainly a very high percentage of applications, it is not enough to simply store information in your variables, as everything will be lost as soon as the application is closed. While the previous chapter covered a wide variety of options for storing data between uses, none of them can quite compare to the versatility, power, and simplicity of Core Data.

There are a great many resources, including entire books (I highly recommend Pro Core Data for iOS, by Michael Privat and Robert Warner) that focus simply on the topic of Core Data. It would be ridiculous to assume that you could cover every part of such a multi-faceted concept in one chapter. For this reason, you will, instead of building your Core Data interface from the ground up, use pre-built Xcode templates in order to provide the easiest demonstration of the Core Data concepts. This way, you, as a developer, can more easily acquire a grasp of Core Data applications, and later focus on more targeted, complex points of the subject through other, more dedicated sources.

What Is Core Data?

Core Data is persistence. Not only is Core Data persistence, but also it is by far one of the best methods for implementing persistence in iOS.

Put simply, Core Data, in conjunction with Xcode, allows a developer to perform three main tasks:

  1. Create a data model
  2. Persist information
  3. Access data

First, it is important to understand exactly what a data model is. This term applies essentially to whatever structure any given application's data is built around. This could be anything as simple as an NSString or an NSArray in a simple application, all the way up to a complex, interconnected system of object types, each with their own properties, methods, and pointers to other objects.

Next, you should have a basic understanding of the various types of objects and classes that are involved in Core Data when you need to create, save, and retrieve information, and how you use them.

  1. NSManagedObjectModel: This is an unusual class, because you don't quite ever deal with it directly when you are writing code if you use a template. This is how iOS refers to your data model, which you will create later. When you create your project for the first recipe shortly, you will see an instance of this type in your application delegate, and you will see it used in some pre-generated methods, but aside from that, you will have no reason to deal with this class programmatically.
  2. NSPersistentStoreCoordinator: This class is one that you very rarely will need to deal with. It works mostly in the background of an application to “coordinate” between your application and the underlying database or “Persistent Store”, but you will not need to send any actions to it. The most important part of this class that you need to know about is the “type” of persistent store that is being used. There are three types of persistent stores:
    1. NSSQLiteStoreType
    2. NSBinaryStoreType
    3. NSInMemoryStoreType

    The default value is the first, NSSQLiteStoreType, specifying that you are using a persistent store built around the SQLite foundation. You will continue to use this type for your applications.

    Depending on your application, you may also find the NSInMemoryStoreType useful, even though it does not actually persist data between application uses. This may be more suited to an application that caches information from a remote source, and thus needs a data model built around Core Data, but does not actually need to store the information, as it can be retrieved again from the remote source.

  3. NSManagedObjectContext: This class, unlike the previous two, is one that you will be dealing with quite often. In the simplest terms, this class acts as a sort of “workspace” for your information. Any time you need to retrieve or store information, you will need a pointer to this class to perform the action. For this reason, a very common practice in Core Data–based applications is to “pass around” a pointer to this class between each part of the application by giving each view controller an NSManagedObjectContext property.
  4. NSFetchedResultsController: This is the primary class for actually “fetching” results through the NSManagedObjectContext. It is not only very powerful, but also very easy to use, especially in conjunction with a UITableView. You will see plenty of examples of using this class throughout this chapter.

You will utilize a variety of other classes specific to Core Data throughout this chapter, but these are more easily explained once you have gone through the creation of your data model.

Recipe 11–1: Creating a Data Model

For this entire chapter, you will create a new project called “MusicSchool”. Make sure to select the Empty Application template, as in Figure 11–1.

Image

Figure 11–1. Creating an empty application to start from scratch

On the next screen, when you enter the project name of “MusicSchool”, be sure to select the box labeled “Use Core Data”. This is the easiest way to get a nice template for using Core Data, which will simplify your life greatly, pre-generating a great deal of necessary code. Set the class prefix to “Main”, and make sure the Use Automatic Reference Counting box is checked as well, as shown in Figure 11–2. The Company Identifier should be changed to your own name or company name.

Image

Figure 11–. Configuring your project to use Core Data

Click Create on the next screen to finish the creation of your project as usual.

Now that you have set up your project to use a Core Data template, you actually have a lot of the work involved in using the Core Data framework already done for you, so you are able to move directly on to building your data model.

As you've probably guessed already, your data model will be built to represent the idea of a music school, specifically focusing on the teachers and students. Before you proceed to do anything in Xcode, you need to plan out exactly how your model will work.

When working with a data model, the first kind of item you have to make is an “entity.” An entity is essentially the Core Data equivalent of an object, and represents an object in the exact same way.

In the same way that objects (or NSObjects in Objective-C) have properties, entities have “attributes.” These are the simpler pieces of data associated with any given entity, such as a name, age, or birthday, that do not require a pointer to any other entity.

Whenever you want one entity to have a pointer to another, you use a “relationship.” A relationship can be either “to-one” or “to-many,” referring to whether an entity has a pointer to one instance of another entity or multiple ones. When dealing with the “to-many” relationship, you will notice that the entity will have a pointer to a set of multiple other entities. Entities can easily have relationships that point to themselves, which might be the case of a Person entity having a relationship to another Person, in the form of a spouse. You can also set up “inverse relationships,” which act as paths back and forth between entities. For example, a Teacher entity might have a “to-many” relationship to a Student entity called “students”, and the Student’s relationship to the Teacher, called “teachers”, will be the inverse of this. With this, you can access any Teacher’s list of Students and their respective list of Teachers quite easily.

So for your data model, you will have three entities with their respective attributes and relationships like so:

  1. Teacher
    1. Attributes: Name, age, primary language, and number of years teaching
    2. Relationships: Students (to-many) and Instruments (to-many)
  2. Student
    1. Attributes: Name, age, primary language, and skill level
    2. Relationships: Teacher (to-one) and Instrument (to-one)
  3. Instrument
    • c. Attributes: Name and family
    • d. Relationships: Students (to-many) and Teachers (to-many)

While all these relationships going back and forth between your objects may seem a bit convoluted, this will actually allow you to create a very well-defined data model from which you can query nearly any piece of information you need from another. For example, you might be able to easily find all the teachers who play the piano, and then access their respective names.

Now that you have your data model planned out, you can build this in Xcode. Switch over to view your data model file, which will probably be called MusicSchool.xcdatamodeld if you used the “MusicSchool” project name. Your view should resemble Figure 11-3.

Image

Figure 11–3. Your empty data model

If your window instead resembles Figure 11-4, you should change to the first Editor style of the two options in the lower-right of the Xcode window. For this recipe, you will use only the first Editor style to actually configure your data model.

Image

Figure 11–4. The graphical Editor style

You can easily change the Editor style for configuring a data model through the selection in the lower right-hand corner of your screen. Select the left option of the two, which will be the first of the two previous figures, so that the selector looks like Figure 11-5.

Image

Figure 11–5. Make sure to select the first Editor style for these recipes.

Next, you will add your three entities. You can do this from either the Editor menu, or by using the Add Entity button in the lower-central area of your view, which resembles Figure 11-6.

Image

Figure 11–6. Use this button to create new entities.

You will immediately be able to change the name of the entity, so rename it to “Teacher” and hit return.

Repeat this twice more to create the “Student” and “Instrument” entities. It is easier to create all your entities first, rather than trying to configure each after you create it, as you need them to build your relationships. Afterward, if you select the Teacher entity, your entities list should resemble Figure 11-7.

Image

Figure 11–7. Rename your entities to match your data plan.

You will start by configuring your Teacher entity, so make sure this one is selected.

Next, under the Attributes area for the Teacher entity, click the + button four times, once for each of your attributes. Name each attribute according to your plan (“name”, “age”, “language”, and “years” will do fine). For each attribute, you must also choose a “Type”. Use the “String” type for the “name” and “language” attributes, and the “Integer 16” type for the others, as in Figure 11-8.

NOTE: The different types of integer (i.e., Integer 16, 32, 64), refer to the number of bits used to store each value. These numbers restrict the highest values you can use, as a 16-bit value can store only values up to 65,535. A 32-bit value can store values up to 4,294,967,295, and a 64-bit can hold massive values on the scale of 1018. Since your numbers are fairly low, you can simply use the “Integer 16” type.

Image

Figure 11–8. Configuring the Teacher entity's attributes

Now under the Relationships area, add two relationships using the + button. Name them “students” and “instruments”, and make sure their destination values are set accordingly, as in Figure 11-9. (“Student” for the students relationship, and so on.) Until you create more relationships in other entities, you cannot set up your “Inverse” relationships yet.

Image

Figure 11–9. Configuring the Teacher entity’s relationships

Pull up the Data Model inspector just as you would pull up the Attributes inspector for any view element in a XIB file. After selecting one of the created relationships, check the box in the inspector labeled “To-Many Relationship”, as in Figure 11-10. Make sure to do this for both of the Teacher relationships, since they both are “to-many” according to your plan.

Image

Figure 11–10. Configuring relationships to be “To-Many”

While you will most likely not need to worry about most of the other values in this inspector (at least for your purposes), one of the values of higher import is the Delete Rule drop-down menu, as shown in Figure 11-10. This value specifies exactly how this relationship is handled when an instance of the given entity is deleted from the NSManagedObjectContext. It has four possible values:

  1. No Action: This is probably the most dangerous value, as it simply allows related objects to continue to attempt to access the deleted object, which could easily lead to accessing problems without proper care.
  2. Nullify: The default value, this specifies that the relationship will be nullified upon deletion, and will thus return a nil value.
  3. Cascade: This value can be slightly dangerous to use, as it specifies that if one object is deleted, all of the objects it is related to via this Delete Rule will also be deleted, so as to avoid having nil values. If you're not careful with this, you can delete unexpectedly large amounts of data, though it can also be very good for keeping your data clean. You may use this, for example, in the case of a “folder” with multiple objects. When a folder is deleted, you would want to delete all the contained objects as well.
  4. Deny: This will prevent the object from being deleted as long as the relationship does not point to nil.

You will keep the Delete Rule on “Nullify” for your recipe.

Now, after selecting the Student entity that you will now configure, add your four attributes (“age”, “language, “name”, and “skill”), with their appropriate types, as in Figure 11-11 (“String” for all but the age, which will be “Integer 16” again).

Image

Figure 11–11. Configuring the Student entity’s attributes

Create another two relationships, “instrument” and “teacher”, with their respective destinations. You can now also set the “Inverse” relationship of the teacher relationship to the value of “students”, as shown in Figure 11-12. The students relationship in your Teacher entity will automatically now be given the teacher relationship you just made as its inverse as well.

NOTE: Inverse relationships are not always required, though they tend to make the organization and flow of your application a little bit better, allowing you to more easily access any piece of data you need from any other piece of data.

If you look back at your original plan for your data model, you will see that the two relationships for the Student are not meant to be “to-many,” so you don't have to make this change.

Image

Figure 11–12. Configuring the Student entity’s relationships

Now, you shall configure your third entity, the Instrument. It will have two attributes of type “String”, called “name” and “family”, as in Figure 11-13.

Image

Figure 11-13. Configuring the Instrument entity’s attributes

You will create your two relationships as “teachers” and “students” with their respective destinations. You should be able to set inverse relationships for both of these, as is done in Figure 11-14.

Image

Figure 11-14. Configuring the Instrument entity’s relationships

Finally, make sure to specify that both of these relationships are “to-many” in the Data Model inspector, just as before.

This is actually all you need to do to create your data model! If you switch over to the second Editor style, you can even see a neat little graphic of your interconnect entities, their attributes, and their relationships, where a single arrow represents a “to-one” relationship, and a double arrow represents a “to-many” relationship. The blocks may initially appear all stacked on top of each other, but if you drag them apart, your display should resemble Figure 11-15.

Image

Figure 11–15. The resulting graphical view of your finished data model

Recipe 11–2: Working with NSManagedObjects

Now that you have your data model set up, you can start to build your application's actual user interface.

You will build your application in such a way that you can view three different tabs, each with a UITableView displaying all of an entity in your data, with each tab displaying a different entity. Rather than build three different view controllers with nearly the exact same setup, you will simply build one, then customize the information displayed.

Create a new UIViewController subclass file called “MainTableViewControlle in your project. In your XIB file, add a UITableView to your view, and connect it to the view controller's header file with the property name tableViewMain. Refer to Chapter 8 for specific instructions on how to do this.

You can go ahead and make this UITableView fill the whole view, and leave it as a “Plain” style, as in Figure 11-16.

Image

Figure 11–16. Setting up your UITableView

Go ahead and set your delegate and data source for your table in the -viewDidLoad method with the following two lines:

self.tableViewMain.delegate = self;
self.tableViewMain.dataSource = self;

You will of course have to add the UITableViewDelegate and UITableViewDataSource protocols to your view controller’s header file.

Add the following line to your -viewDidLoad method as well to create an Add button in your navigation bar. You will implement the action for it to perform later.

self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(add)];

You will need to add a few more properties to your view controller to keep track of your Core Data objects. Add the following three properties, making sure to properly synthesize and handle them.

@property (nonatomic, strong) NSEntityDescription *entityDescription;
@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;

An instance of the NSEntityDescription class is, in rather redundant terms, a description of an entity. In its simplest use, you simply give it a name. Then, when you query your database via the NSManagedObjectContext with this NSEntityDescription, it specifically fetches instances of the specified entity.

Your entityDescription property will allow you to easily keep track of whether a view controller is fetching data for Teachers, Students, or Instruments. The fetchedResultsController will keep track of your fetched data, and the managedObjectContext property allows you to make any necessary requests for data.

Next, you need to implement the required delegate and data source methods for your UITableView. First, the method to specify the number of rows:

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
     return [[self.fetchedResultsController fetchedObjects] count];
}

As shown, the NSFetchedResultsController class contains a method -fetchedObjects, which returns an NSArray of the objects that were queried for.

Here is your method to configure your UITableView’s cells:

-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
{
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
        cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
        cell.textLabel.font = [UIFont systemFontOfSize:19.0];
        cell.detailTextLabel.font = [UIFont systemFontOfSize:12];
    }
    
    NSManagedObject *object = [self.fetchedResultsController
objectAtIndexPath:indexPath];
    cell.textLabel.text = [object valueForKey:@"name"];
    
    if ([object.entity.name isEqualToString:@"Instrument"])
    {
        cell.detailTextLabel.text = [object valueForKey:@"family"];
    }
    else
    {
        cell.detailTextLabel.text = [[object valueForKey:@"age"] stringValue];
    }
        return cell;
}

As you can see, you are able to access attributes of your entities by using the NSManagedObject class to point to an instance of an entity, then using the -valueForKey: method to query specific attributes. You will later see an even easier, more natural way to do this, but it is important to understand the concepts of NSManagedObject as a class that can represent any entity.

You may also notice that you don't have to access the row value of the indexPath to give to your NSFetchedResultsController. This is one of the niceties of using Core Data, in that the results of a fetch request are very easy to implement into a UITableView.

Now that your UITableView is set up, you simply need to make sure that your view controller actually gets the information that it needs to display. You will set up a separate method for this like so:

-(void)fetchResults
{
    NSFetchRequest *fetchRequest = [NSFetchRequest
fetchRequestWithEntityName:self.entityDescription.name];
    NSString *cacheName = [self.entityDescription.name
stringByAppendingString:@"Cache"];
    
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name"
ascending:YES];
    [fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
    
    self.fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil cacheName:cacheName];
    BOOL success;
    NSError *error;
    success = [self.fetchedResultsController performFetch:&error];
    if (!success)
    {
        NSLog(@"%@", [error localizedDescription]);
    }
}

If you need to, add the handler of this method to your header file to avoid any compiler warnings (i.e., if you implement this method after your -viewDidLoad).

Considering the importance of the previous method, it's important to understand the exact steps required in order to perform the “fetch” for data.

  1. The first thing you need for a fetch is an instance of the NSFetchRequest class. Here, you have used a designated initializer to specify an NSEntityDescription, though you can also add it later using the -setEntity: method.
  2. While not required, you have set up a “cache name” to be used with your fetch request, with a different cache for each entity. This allows you to slightly improve the speed of your application if you are making frequent fetch requests, as a local cache is first checked to see if the request has already been performed.
  3. Every instance of NSFetchRequest is required to have at least one NSSortDescriptor associated with it. Here, you have specified a very simple alphabetic sort of the name property for each of your entities. Once all your NSSortDescriptors have been created, they must be attached to the NSFetchRequest using the -setSortDescriptors: method.
  4. Once your NSFetchRequest is fully configured, you can fully initialize your NSFetchedResultsController using your NSFetchRequest and your NSManagedObjectContext. The last two parameters are both optional, though you have specified a cacheName for optimization. You can set both of these to nil if you wish to ignore them.
  5. Finally, you must use the performFetch: method to actually complete your fetch request and retrieve your stored data. With this method, you can pass a pointer to an NSError, as shown previously, to keep track of and log any errors that occur with a fetch.

Finally, you will add a quick line to call this method in the -viewDidLoad method.

[self fetchResults];

In entirety, your -viewDidLoad method should look like so:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.tableViewMain.delegate = self;
    self.tableViewMain.dataSource = self;
    
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(add)];
    
    [self fetchResults];
}

Now that you have finished configuring your view controller, you need to go back to your application delegate and set up your basic navigation system to use it.

First, in your application delegate header file, add an import statement for your view controller's header file to appease the compiler.

#import "MainTableViewController.h"

Now you will declare all of your view controllers as properties of your application delegate. You will also set up each view controller in a UINavigationController, and all three of these inside a UITabBarController, just to get a nice flow of information. Add all the following properties to your application delegate, making sure to synthesize them as always.

@property (strong, nonatomic) MainTableViewController *teacherTable;
@property (strong, nonatomic) MainTableViewController *studentTable;
@property (strong, nonatomic) MainTableViewController *instrumentTable;
@property (strong, nonatomic) UINavigationController *teacherNavcon;
@property (strong, nonatomic) UINavigationController *studentNavcon;
@property (strong, nonatomic) UINavigationController *instrumentNavcon;

@property (strong, nonatomic) UITabBarController *tabBarController;

Now you need to change up your -application:didFinishLaunchingWithOptions: method in the Application Delegate class to correctly configure all of your view controllers. Overall, it will look like so:

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor];
    
    self.teacherTable = [[MainTableViewController alloc] init];
    self.teacherTable.entityDescription = [NSEntityDescription entityForName:@"Teacher"
inManagedObjectContext:self.managedObjectContext];
    self.teacherTable.managedObjectContext = self.managedObjectContext;
    self.teacherTable.title = @"Teachers";
    
    self.studentTable = [[MainTableViewController alloc] init];
    self.studentTable.entityDescription = [NSEntityDescription entityForName:@"Student"
inManagedObjectContext:self.managedObjectContext];
    self.studentTable.managedObjectContext = self.managedObjectContext;
    self.studentTable.title = @"Students";
    
    self.instrumentTable = [[MainTableViewController alloc] init];
    self.instrumentTable.entityDescription = [NSEntityDescription entityForName:@"Instrument" inManagedObjectContext:self.managedObjectContext];
    self.instrumentTable.managedObjectContext = self.managedObjectContext;
    self.instrumentTable.title = @"Instruments";
    
    self.teacherNavcon = [[UINavigationController alloc]
initWithRootViewController:self.teacherTable];
    self.studentNavcon = [[UINavigationController alloc]
initWithRootViewController:self.studentTable];
    self.instrumentNavcon = [[UINavigationController alloc]
initWithRootViewController:self.instrumentTable];
    
    self.tabBarController = [[UITabBarController alloc] init];
    [self.tabBarController setViewControllers:[NSArray
arrayWithObjects:self.teacherNavcon, self.studentNavcon, self.instrumentNavcon, nil]];
    
    [self.window addSubview:self.tabBarController.view];
    [self.window makeKeyAndVisible];
    return YES;
}

The configuration for each of your MainTableViewControllers was fairly simple, with all you had to do being to set up an NSEntityDescription, give it a pointer to your NSManagedObjectContext, and then give it a title to display in your UITabBarController.

At this point, if you run your application, all you'll see is a few empty tables (just as in Figure 11-17), and with good reason. You don't have any data yet! Unfortunately, you haven't built your -add method yet, so don't go pressing that + button until you do, since you will crash your application.

Image

Figure 11-17. Your three empty UITableViews

For your preliminary recipe, you can start by simply programmatically creating a new object in each view controller to be displayed. Add the following code to your MainViewController's -viewDidLoad method, making sure it is before the call to -fetchResults.

NSManagedObject *add = [[NSManagedObject alloc] initWithEntity:self.entityDescription
insertIntoManagedObjectContext:self.managedObjectContext];
    if (![self.entityDescription.name isEqualToString:@"Instrument"])
    {
        [add setValue:@"Jim" forKey:@"name"];
        [add setValue:[NSNumber numberWithInt:42] forKey:@"age"];
    }
    else
    {
        [add setValue:@"Trumpet" forKey:@"name"];
        [add setValue:@"Brass" forKey:@"family"];
    }

Now when you run the app, some temporary data will be displayed, as shown by Figure 11-18.

Image

Figure 11–18. Your semi-populated UITableView

While you have created a new instance of each of your entities in your NSManagedObjectContext, you'll probably notice that these objects are not persisting every time you run this application, as you are not seeing an increasing list with every run. This is because you did not save the changes you made to the NSManagedObjectContext. You can do this by adding the following after the previous additions, but still before the call to -fetchRequest.

NSError *error;
    BOOL success = [self.managedObjectContext save:&error];
    if (!success)
    {
        NSLog(@"%@", [error localizedDescription]);
    }

You'll notice that this is very similar to the -performFetch: method that you used in -fetchResults, in that you send it a pointer to an instance of NSError in order to log any issues. As with before, this is totally optional, and you could pass nil as this parameter, but for best practices it is safer to include the logging.

If you run your application a few times, changing the name, you can accumulate a few different pieces of data to display. Figure 11-19 shows your application after having been run a few times to collect some data.

Image

Figure 11–19. Your app preserving and creating new data

To make your program run a bit better, you can go ahead and implement that -add method, so that you can create new data from inside the app. Typically, you would want to create separate view controllers to allow the user to input the information for a new object, but for your purposes, the key concept is simply to be able to add objects.

-(void)add
{
    NSManagedObject *add = [[NSManagedObject alloc]
initWithEntity:self.entityDescription
insertIntoManagedObjectContext:self.managedObjectContext];
    if (![self.entityDescription.name isEqualToString:@"Instrument"])
    {
        [add setValue:@"Peter" forKey:@"name"];
        [add setValue:[NSNumber numberWithInt:35] forKey:@"age"];
    }
    else
    {
        [add setValue:@"Guitar" forKey:@"name"];
        [add setValue:@"Strings" forKey:@"family"];
    }
    
    NSError *error;
    BOOL success = [self.managedObjectContext save:&error];
    if (!success)
    {
        NSLog(@"%@", [error localizedDescription]);
    }
    
    [self fetchResults];
    
    [self.tableViewMain reloadData];
}

In the previous method, you must make sure to call the -fetchResults method once you are done creating the new object, but before you reload your UITableView's data, in order to make sure that your NSFetchedResultsController contains the most recent changes.

Just as Core Data makes a UITableView very easy to populate, it is also very easy to implement deletion of items from a UITableView in conjunction with the Core Data classes. You can implement your UITableView's -tableView:commitEditingStyle:forRowAtIndexPath: like so:

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        NSManagedObject *deleted = [self.fetchedResultsController
objectAtIndexPath:indexPath];
        [self.managedObjectContext deleteObject:deleted];
        NSError *error;
        BOOL success = [self.managedObjectContext save:&error];
        if (!success)
        {
            NSLog(@"%@", [error localizedDescription]);
        }
        [self fetchResults];
        [self.tableViewMain deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationRight];
    }
}

While it is certainly possible to allow deletion only by having the user swipe over a row, it is generally a good idea to also provide an Edit button, in case the user is unfamiliar with the swiping functionality. To do this, you first need to make a simple adjustment to your -viewDidLoad method to create the button, which will now resemble the following:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.tableViewMain.delegate = self;
    self.tableViewMain.dataSource = self;
    
    /////Adjusted code to add Edit button
    UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(add)];
    UIBarButtonItem *editButton = self.editButtonItem;
    self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:addButton,
editButton, nil];
    /////End of adjusted code
    
    [self fetchResults];
}

Now you just need a quick implementation of the -setEditing:animated: method to get your nice little animations correct.

-(void)setEditing:(BOOL)editing animated:(BOOL)animated
{
    [super setEditing:editing animated:animated];
    [self.tableViewMain setEditing:editing animated:animated];
}

Upon running your app now, you'll notice you can not only add, but also delete information from your table, just as is done in Figure 11-20.

Image

Figure 11-20. Your app, adding and deleting data

Recipe 11–3: Subclassing NSManagedObject

Now that you have your basic UITableView set up to display the information you can store with Core Data, it's time to improve a bit on your program design. The first thing you can do is an incredible time saver that will make programming with Core Data significantly easier. You will be subclassing the NSManagedObject class for each of the entities that you created. Unlike most subclassing, however, this is incredibly easy to set up.

First, switch back to view your MusicSchool.xcdatamodeld file in which you created your entities.

The next step can be done individually, but you will create your three subclasses all at once. Select each entity from the view by holding the “command” key and clicking each one, so that all three become highlighted. You will also notice that all of their combined attributes and relationships will be visible as well, as shown in Figure 11-21.

The NSManagedObject subclassing technique that you will be using creates a subclass based on which entity or entities are selected, so it is important to make sure that all the entities you want subclasses for are selected.

Image

Figure 11–21. Selecting all three entities to be turned into NSManagedObject subclasses

Next, under the File menu, select New ImageNew File... to bring up the New File dialog. Navigate to the Core Data section under iOS, and select the “NSManagedObject subclass” option, as shown in Figure 11-22.

Image

Figure 11-22. Selecting the “NSManagedObject subclass” template to automatically generate classes for your entities

Click through, and hit Create to have Xcode create your three NSManagedObject subclasses.

By subclassing the NSManagedObject class, you have effectively turned your entities into classes that you can use and manipulate normally, especially when it comes to accessing their attributes. Now, rather than having to use the -setValue:forKey: and -valueForKey: methods, you can simply use the setter and getter methods, or, more simply, the properties created with the same names as your attributes. This also allows you to more easily specify exactly which entity any given instance of NSManagedObject is associated with, so that your compiler can assist you in accessing properties and methods for that entity.

To demonstrate these abilities, you can rewrite your -tableView:cellForRowAtIndexPath: method to first cast your NSManagedObject down to its appropriate subclass, and then populate your cell’s content using the subclass properties.

First, make sure to import the newly created header files into your view controller.

#import "Instrument.h"
#import "Student.h"
#import "Teacher.h"

Once this is taken care of, you can implement your simplified method.

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
        cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
        cell.textLabel.font = [UIFont systemFontOfSize:19.0];
        cell.detailTextLabel.font = [UIFont systemFontOfSize:12];
    }
    
    NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
    
    if ([object.entity.name isEqualToString:@"Instrument"])
    {
        Instrument *instrument = (Instrument *)object;
        cell.textLabel.text = instrument.name;
        cell.detailTextLabel.text = instrument.family;
    }
    else if ([object.entity.name isEqualToString:@"Student"])
    {
        Student *student = (Student *)object;
        cell.textLabel.text = student.name;
        cell.detailTextLabel.text = [student.age stringValue];
    }
    else
    {
        Teacher *teacher = (Teacher *)object;
        cell.textLabel.text = teacher.name;
        cell.detailTextLabel.text = [teacher.age stringValue];
    }
    
    return cell;
}

You can also simplify your -add method that creates your test data like so.

-(void)add
{
    NSManagedObject *add = [[NSManagedObject alloc]
initWithEntity:self.entityDescription
insertIntoManagedObjectContext:self.managedObjectContext];
    if ([self.entityDescription.name isEqualToString:@"Teacher"])
    {
        Teacher *teacher = (Teacher *)add;
        teacher.name = @"Peter";
        teacher.age = [NSNumber numberWithInt:36];
    }
    else if ([self.entityDescription.name isEqualToString:@"Instrument"])
    {
        Instrument *instrument = (Instrument *)add;
        instrument.name = @"Guitar";
        instrument.family = @"Strings";
    }
    else
    {
        Student *student = (Student *)add;
        student.name = @"Andrew";
        student.age = [NSNumber numberWithInt:18];
    }
    
    NSError *error;
    BOOL success = [self.managedObjectContext save:&error];
    if (!success)
    {
        NSLog(@"%@", [error localizedDescription]);
    }
    
    [self fetchResults];
    
    [self.tableViewMain reloadData];
}

Again, this doesn’t really affect how your application runs, but it makes your life a great deal easier in terms of coding and debugging.

By using this subclassing technique, you will probably find yourself having fewer issues with runtime errors when using Core Data, as your compiler will now be able to confirm that you are accessing the correct properties for any given subclass.

Just as you are able to access and edit the attributes of your subclassed NSManagedObjects, you can do the same with the relationships that your entities share. You will change your program such that if a Teacher is selected, a list of Instruments entered appears, and when one of these Instruments is selected, that Instrument is added to the set of instruments for that Teacher. You may also notice that because you have your relationships set up with inverses to each other, a relationship set one way will also be set in the reverse. In your case, when you add the Instrument into the instruments set of a Teacher, that Teacher will also be added into the teachers set of the Instrument.

For demonstration purposes, you will implement this behavior only between Teachers and Instruments, but it should be easy to see how you would fully implement similar behavior between all three entities, allowing the user to connect any Teacher with his or her Students and Instruments.

First, you will declare an instance variable in your MainTableViewController’s header file of type Teacher, to help keep track of the selected Teacher.

__strong Teacher *selectedTeacher;

Next, you will need to declare a new property called delegate of the same type as the class, which you will use to connect your multiple view controllers. Make sure to synthesize and nil this property as appropriate.

@property (nonatomic, strong) MainTableViewController *delegate;

Declare also the header for a method for your delegate property to perform, like so:

-(void)MainTableViewController:(MainTableViewController *)mainTableVC
didSelectInstrument:(Instrument *)instrument;

Your header file (MainTableViewController.h) should now resemble the following:

#import <UIKit/UIKit.h>
#import "Instrument.h"
#import "Student.h"
#import "Teacher.h"

@interface MainTableViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>{
    
    __strong Teacher *selectedTeacher;
}

@property (strong, nonatomic) IBOutlet UITableView *tableViewMain;

@property (nonatomic, strong) NSEntityDescription *entityDescription;
@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;

@property (nonatomic, strong) MainTableViewController *delegate;

-(void)MainTableViewController:(MainTableViewController *)mainTableVC didSelectInstrument:(Instrument *)instrument;

@end

Next, you will use the delegate property in conjunction with your UITableView’s row selection method to build your new, albeit selective, functionality.

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [self.tableViewMain deselectRowAtIndexPath:indexPath animated:YES];
    if ([self.entityDescription.name isEqualToString:@"Teacher"])
    {
        selectedTeacher = [self.fetchedResultsController objectAtIndexPath:indexPath];
        MainTableViewController *selectInstrument = [[MainTableViewController alloc] init];
        selectInstrument.entityDescription = [NSEntityDescription entityForName:@"Instrument" inManagedObjectContext:self.managedObjectContext];
        selectInstrument.managedObjectContext = self.managedObjectContext;
        selectInstrument.delegate = self;
        [self.navigationController pushViewController:selectInstrument animated:YES];
    }
    else if ([self.entityDescription.name isEqualToString:@"Instrument"] && (self.delegate != nil))
    {
        [self.delegate MainTableViewController:self didSelectInstrument:[self.fetchedResultsController objectAtIndexPath:indexPath]];
        [self.navigationController popViewControllerAnimated:YES];
    }
}

You can now implement the delegate method you recently declared to add the chosen Instrument to the selectedTeacher, and then save the changes to the NSManagedObjectContext.

-(void)MainTableViewController:(MainTableViewController *)mainTableVC didSelectInstrument:(NSManagedObject *)instrument
{
    [selectedTeacher addInstrumentsObject:instrument];
    NSError *saveError;
    BOOL success = [self.managedObjectContext save:&saveError];
    if (!success)
    {
        NSLog(@"%@", [saveError localizedFailureReason]);
    }
}

Since you didn’t flesh out a full system for the user to create custom data, you will need to adjust your -add method a few times, running your application each time, to create some different data in order to fully test this functionality.

Recipe 11–4: Filtering Your Fetch Requests

When you’re using your instances of NSFetchRequest to get information from your NSManagedObjectContext, you are in no way limited to only requesting all of a specific entity. Through the use of the NSPredicate class, you can easily refine your results to nearly any subset depending on your application.

For your application, you will implement your filtering behavior to be applied upon the tapping of the Accessory button in your UITableViews, with different actions depending on the type of NSManagedObject tapped.

  • If the Accessory button is tapped in your Teachers table, you will display another UITableView of all the instruments that the selected Teacher is associated with.
  • Tapping the accessory button in your Students table will display a table of all other students of the same age as the one selected.
  • In your Instruments table, you will filter your data to display all other Instruments with the same family as the selected one.

All of these behaviors will be implemented by setting an NSPredicate to your NSFetchRequest with the specified predicate.

First, you will add a property to your MainTableViewController to keep track of this NSPredicate so that it can be easily created and used to perform fetch requests.

@property (nonatomic, strong) NSPredicate *predicate;

You can implement your UITableView’s delegate method like so:

-(void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
{
    if (![self.title isEqualToString:@"Filtered"])
    {
        MainTableViewController *filtered = [[MainTableViewController alloc] init];
        filtered.title = @"Filtered";
        filtered.managedObjectContext = self.managedObjectContext;
    
        if ([self.entityDescription.name isEqualToString:@"Teacher"])
        {
            filtered.entityDescription = [NSEntityDescription entityForName:@"Instrument" inManagedObjectContext:self.managedObjectContext];
            NSSet *instruments = [(Teacher *)[self.fetchedResultsController objectAtIndexPath:indexPath] instruments];
            filtered.predicate = [NSPredicate predicateWithFormat:@"self IN %@", instruments];
        }
        else if ([self.entityDescription.name isEqualToString:@"Student"])
        {
            filtered.entityDescription = self.entityDescription;
            filtered.predicate = [NSPredicate predicateWithFormat:@"age=%i", [[(Student *)[self.fetchedResultsController objectAtIndexPath: indexPath] age] intValue]];
        }
        else
        {
            filtered.entityDescription = self.entityDescription;
            filtered.predicate = [NSPredicate predicateWithFormat:@"family=%@", [(Instrument *)[self.fetchedResultsController objectAtIndexPath:indexPath]family]];
        }
    
        [self.navigationController pushViewController:filtered animated:YES];
    }
}

As you can see, you have used a couple of different styles of predicates in your implementation. You have a simple comparing predicate comparing the family property with a value, and then you have a predicate to check for an object contained in a set for the Teacher.

Whenever you need to specify a certain value in a predicate, you can use “%@”, and then pass the value afterward, as shown in the previous code block. In case you are unfamiliar with these kinds of values, called “format specifiers,” some of the more common ones include the following:

  • %@: This can be used to represent either a NSString value, or simply a reference to an object, as you used previously.
  • %i: This represents an integer.
  • %f: This represents a float.
  • %d: This represents a double value.

These specifiers, which originated in the C programming language for printing formatted strings, are most often used in conjunction with either the NSLog() command or when displaying information for the user. For example, you may have a piece of code for testing to log a simple counter, which would look like so:

for (int n=0; n < 100; n++)
    {
        NSLog(@"%i", n);
    }

Whenever you are referring to the object being evaluated with a predicate, you use the keyword “self”, as shown in the first NSPredicate.

There is a great deal of documentation on creating NSPredicates with different formats and methods, which allow the developer a great deal of power in creating filters for their results. Refer to the Apple documentation for more information on creating more complex predicates.

Just before you are quite finished with this setup, you also need to modify your -fetchResults method to take the NSPredicate into account. The new method will look like so:

-(void)fetchResults
{
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:self.entityDescription.name];
    NSString *cacheName = [self.entityDescription.name stringByAppendingString:@"Cache"];
    
    ////////////New Predicate code
    if (self.predicate != nil)
    {
        [fetchRequest setPredicate:self.predicate];
    }
    ////////////End of new code
    
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
    [fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
    
    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:cacheName];
    
    BOOL success;
    NSError *error;
    success = [self.fetchedResultsController performFetch:&error];
    if (!success)
    {
        NSLog(@"%@", [error localizedDescription]);
    }
}

At this point, if you run your application, you can see some examples of the filtered results, such as the filtering of Instruments by family in Figure 11-23.

Image

Figure 11-23. Your app displaying filtered results

Recipe 11–5: Versioning

For almost any application, you will most likely need to make changes to your Core Data model at one point or another. Xcode gives you a very nice, easy way to do this in such a way that you can keep all your old models easily in case you have any issues, through a process called “versioning.”

The first step you will take is to create a new version of your data model, which you can base off of the one you already have. First, select your MusicSchool.xcdatamodeld file in your navigator pane on the left.

In the Editor menu, select Add Model Version…. A simple dialog will appear, allowing you to specify the name of the new version model, as well as which model to base the new one off of. Since you have only the one model so far, it will be your only choice. Name the new model “MusicSchool2”, as in Figure 11-24.

Image

Figure 11-24. Creating a new version

Click Finish, and your new model will be created.

Now, you can make some changes to your data model. For the purposes of your application, you will be making only fairly simple changes so that your migration, which you will deal with shortly, is a simple process. In order to keep with a lightweight migration, you should stick to only the following possible changes:

  • Adding or removing entities, attributes, or relationships
  • Renaming properties or entities
  • Changing whether an attribute is optional

For your new data model, you will simply add two attributes: first, an NSString called size to your Instrument entity, and another NSString called range, also to the Instrument entity. The resulting attributes table will resemble Figure 11-25.

Image

Figure 11-25. Adding new attributes to your Instrument entity

Next, you need to specify this newest model version to be the one that your application actually uses. First, select the top level of your .xcdatamodel files, which will be listed as having the file type “xcdatamodeld”, and have the drop-down menu of your two actual data model files, as in Figure 11-26. (This one specifies which of the two is actually being selected.)

Image

Figure 11-26. Viewing your project’s data models; the currently used one is listed as the main file.

Next, you need to bring up the File inspector. You can do this either by navigating from the View menu to Utilities Image Show File Inspector, or by bringing up the Utilities pane on the right side of the screen, and then selecting the first tab, as shown in Figure 11-27.

CAUTION: Xcode does not currently allow you to delete data model files from your project. Be careful when creating new versions, as you will not be able to easily remove them once they are created. In Figure 11-27, you can see a buildup of multiple data model files that are not needed or used.

Image

Figure 11-27. Using the File inspector to set the current version

Under the Versioned Core Data Model section, you need to change the current model to your new “MusicSchool2”, specifically shown in Figure 11-28.

Image

Figure 11-28. Selecting a different current version

At this point, you will notice that a small green check mark has been placed next to the new model version, signifying that it is now the current version, as in Figure 11-29.

Image

Figure 11-29. Your new model file set as the current version

If you try to run this application immediately, depending on how complex your changes were, your application might possibly crash upon running with a very strange reason of “The model used to open the store is incompatible with the one used to create the store”. This essentially means that you are trying to migrate data from an old model to a new one, and it does not know what to do. Luckily, for those lightweight migrations, this is easy to fix.

Navigate over to your application delegate implementation file, and find the -persistentStoreCoordinator method. Look for the following line:

if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])

You will specify an instance of NSDictionary to pass as the options parameter of this method. Add the following code before the previous if statement.

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];

Make sure to set this dictionary as the actual parameter so that the if statement reads like so now:

if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])

Any lightweight migrations will now automatically take place, making your job immensely easier.

If your app runs perfectly fine without this change, then you do not need to worry about this step, but know that you may have to at some point.

An Irritating Error

Sometimes when dealing with versioning, you may run into a problem where an error is thrown by the method that states that the store could not be found. An easy, albeit frustrating solution to this is to delete your persistent store using the following command in the event of an error by adding this line inside the previously referenced if statement.

[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];

Your if statement would then resemble the following code in this case. Keep in mind that you do not at all need or want to add this code unless you encounter this specific problem.

if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])
    {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];

        abort();
    }

After running this application once, your persistent store will be reset, so you should be able to remove this line from the method once your application is working correctly again. This way, your app does not go and reset your data without your explicit expectation.

While this should fix the problem, you will unfortunately lose all of your saved data, so it is generally best to avoid this drastic measure.

At this point, your application will work again, and you can add code to take advantage of your newer model version. However, since you have not updated your NSManagedObject subclasses, you will not be able to access any of your new attributes without using the -valueForKey: method. Since you made changes only to the Instrument entity, you will refresh only your Instrument subclass.

First, delete the Instrument subclass files. You can choose to fully delete the files, rather than simply their references, since you will not need this version of it anymore, and you can always re-create it from the data model again if you have to revert. The dialog with which to do this will resemble Figure 11-30.

Image

Figure 11-30. Deleting your Instrument subclass files; use the Delete option, as you will re-create them.

Now you can go through the exact same steps as before to create your subclass.

In your data model file, select the Instrument entity, then navigate to File Image New Image New File....

Under Core Data for iOS, choose the “NSManagedObject subclass” template (as in Figure 11-31), and then click through to create your new subclass.

Image

Figure 11-31. Re-creating your NSManagedObject subclass for your newest version

You’ll now see that your Instrument class contains all the necessary properties and methods to handle the added attributes in your new, versioned data model!

Summary

Throughout this chapter, we have covered the basics of Core Data, one of the most integral parts of iOS development, due to its power and simplicity of use when it comes to data modeling, persistence, and access. However, we have by no means detailed every facet in the Core Data framework, or even touched on every general subject related to it. You can easily find entire books devoted to the subject of Core Data, and you probably should, in order to get a more complete view of exactly how much ability you have in controlling how your data is stored. The overview here has demonstrated a basic use of the framework and explained the key concepts needed to get started working with Core Data, so that you can implement simple persistence in your applications without worrying about the more esoteric complexities.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset