Compiled Chronicles

A software development blog by Angelo Villegas

iOS: Introduction To Core Data

In this tutorial, you will learn how to work with Core Data and how to model your data for an enterprise-level apps. We will not program or anything but you will need Xcode for the modeling part of the tutorial.

Note: You can download Xcode 4 at http://developer.apple.com/ or via the Mac App Store

In this tutorial, I will mainly use Xcode 4 so some interface might be different with the one you’re using.

Open Xcode and create a new Navigation-based Application project but before clicking the “Choose…” button, make sure the Use Core Data for Storage is checked. Name your project Core Data Modeling (or whatever you want). I will be using Core Data Modeling for the entire tutorial.

Xcode 3

Look at the Resources folder and you’ll notice a .xcdatamodeld. Click the left disclosure triangle besides .xcdatamodeld and you’ll find another .xcdatamodel without a d at the end. Always remember that, xcdatamodeld is a package file that contains the core data files and xcdatamodel are the core data model.

Xcode 4

If you’re using Xcode 4, don’t bother searching the Resource folder because it’s not present anymore. The .xcdatamodeld is now located in the main folder and it is now a package file that has no disclosure button.

Note: If you click Show Package Contents on Finder, you will see that all core data models are inside (just like an ordinary folder).

Now, single-click (or double-click if you want a separate window editor) CoreDataModeling.xcdatamodel and you’ll see the Core Data Mapping Model Editor. This is the Xcode’s built-in graphical editor for Core Data.

Xcode tool uses a multi-pane interface, as shown in the image below. There are three known panes or mapping components, the Entity mappings list, Property mapping tables, and New Entity mapping management area.

The entity mapping list shows all the entity mappings defined in the model. You can edit the name of an entity mapping by double-clicking the text.

The property mapping tables consists of three groups:

  • Attributes
  • Relationships
  • Fetched Properties

The new entity mapping management area allows you to add a new entity mapping to the model. In this pane, you will see the Add Entity and Add Attribute buttons, and the Outline Style and Editor Style segmented controls.

By clicking on the Graph view of the editor style, you will see something similar to this:

But enough with the view, return to the Table Editor and examine again the tables: Entities, Fetch Request, Configuration, Attributes, Relationships, and Fetched Properties with a default value of Event and timeStamp for the Entities and Attributes. You can actually run it as is without changing the original, template code. Once the iPhone simulator is running, you will see something similar to this:

If you tap the “plus” button, a time stamp will appear recording when you tapped the “plus” button.

Now, try to push the Home Button and open the app again, You’ll notice that the data is still in the app. This is one way of holding and storing data of user input from the app to SQLite (Yes, even using Core Data, it still uses SQLite as it’s database). I will not go through that so if you don’t have any idea about Core Data and SQLite, I suggest read some books or online resource first.

Let’s look at the code, shall we?

Examining RootViewController

Close the simulator and open up Xcode again, go to the RootViewController.m first. You’ll first notice the NSFetchedResultsControllerDelegate protocol. You can see this in the RootViewController.h

Search for the - (NSFetchedResultsController *)fetchedResultsController method.

- (NSFetchedResultsController *)fetchedResultsController
{
    if (__fetchedResultsController != nil)
    {
        return __fetchedResultsController;
    }
    
    /*
     Set up the fetched results controller.
    */
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName: @"Event" inManagedObjectContext: self.managedObjectContext];
    [fetchRequest setEntity: entity];
    
    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize: 20];
    
    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey: @"timeStamp" ascending: NO];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: sortDescriptor, nil];
    
    [fetchRequest setSortDescriptors: sortDescriptors];
    
    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest: fetchRequest managedObjectContext: self.managedObjectContext sectionNameKeyPath: nil cacheName: @"Root"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;
    
    [aFetchedResultsController release];
    [fetchRequest release];
    [sortDescriptor release];
    [sortDescriptors release];

	NSError *error = nil;
	if (![self.fetchedResultsController performFetch: &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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
	     */
	    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
	    abort();
	}
    
    return __fetchedResultsController;
}

The first part of the code should be familiar to you. It checks to see if you already have a fetchedResultsController. If it exists, the method returns the fetchedResultsController, if not, the code creates it.

The next section of the code creates and configures the objects needed by the fetchedResultsController. If you look at the code, you need a NSFetchRequest and a NSManagedObjectContext to be able to use the NSFetchedResultsController.

Note: You can think of a NSFetchRequest as a SELECT statement of SQL. The code creates a NSFetchRequest object, creates an entity based on the “Event” entity in the NSManagedContext, then sets the entity used by the NSFetchRequest

The code sets the batch sie of the NSFetchRequest to a number of records to receive at a time. Next, it creates an NSSortDescriptor. NSSortDescriptor is used to sort the results in the NSFetchRequest.

Note: Think NSSortDescriptor as the ORDER BY. Order the result based on the timeStamp attribute in descending order.

Last will be calling the initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName method. It creates, and initializes the NSFetchedRequestsController property.

Note: Notice all the objects here that is being released at the end. Make sure to release every objects you created. Memory Management is crucial when dealing with Mobile Apps

If you examine the whole code, there are four delegate methods that has been used by the template, the controllerWillChangeContent:, controller:didChangeSection:, controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:, controllerDidChange: method. The NSFetchedResultsController calls this method when all changes to the objects managed by the controller are complete.

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

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

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

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

Moving forward, there are still some codes that you might not know. One of these codes can be found inside some methods that you already know such as by looking at the numberOfSectionsTable: method:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
	return [[self.fetchedResultsController sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	id  sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
	return [sectionInfo numberOfObjects];
}

If you don’t know yet, numberOfSectionsTable: returns how many sections should the tableView hold. In this case, we’ll get how many sections fetchedResultsController have and then return it’s count. Then, numberOfRowsInSection returns how many rows should the tableView hold. Again, we’ll get how many objects are stored inside the fetchedResultsController and then return it’s count by sending the property numberOfObjects.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

	// Configure the cell.
	[self configureCell:cell atIndexPath:indexPath];
    return cell;
}

Now, we will look at how the template display the data. If you already know how to work with UITableView, then you must already know how the code works. Inside this method calls another method customized by the template: - configureCell:atIndexPath:

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

This method creates a NSMangedObject that gets the object in the fetchedResultsController, and then sets the cell’s textLabel by calling the getter method valueForKey: with the key string timeStamp and returning it’s description.

Last will be the insertNewObject method. This method, well, insert a new object inside the database.

- (void)insertNewObject
{
    // Create a new instance of the entity managed by the fetched results controller.
    NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
    NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
    NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
    
    // If appropriate, configure the new managed object.
    // Normally you should use accessor methods, but using KVC here avoids the need to add a custom class to the template.
    [newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];
    
    // Save the context.
    NSError *error = nil;
    if (![context 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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
         */
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
}

This method inserts a new object by setting the method - insertNewObjectForEntityForName:inManagedObjectContext: and calling setValue:forKey: method.

Examining CoreDataModelingAppDelegate.m

There’s actually not much of explaining to do here. Most of these new codes are self explanatory. So I will left out the rest and just tell you the method - (NSPersistentStoreCoordinator *)persistentStoreCoordinator

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (__persistentStoreCoordinator != nil)
    {
        return __persistentStoreCoordinator;
    }
    
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataModeling.sqlite"];
    
    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
         
         Typical reasons for an error here include:
         * The persistent store is not accessible;
         * The schema for the persistent store is incompatible with current managed object model.
         Check the error message to determine what the actual problem was.
         
         
         If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory.
         
         If you encounter schema incompatibility errors during development, you can reduce their frequency by:
         * Simply deleting the existing store:
         [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]
         
         * Performing automatic lightweight migration by passing the following dictionary as the options parameter: 
         [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
         
         Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details.
         
         */
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }    
    
    return __persistentStoreCoordinator;
}

This method creates a persistent object and returning with the objects inside CoreDataModeling.sqlite. Surprised? I thought so. Core Data is a framework that makes data related tasks easier and it is not a database. Many programmers still makes mistakes saying that Core Data is a storage framework, etc. It’s not. Core Data uses SQLite as it’s storage for data.

Wrapping things up

Are you still with me? I hope this introduction is not boring. You sure learned a lot in a day. We didn’t edited or updated the codes here, we’ve just learned what the simplest and basic functions we need to know to work with Core Data.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *