iOS Core Data

Create a Core Data Model with Xcode 4.2

  • Create a new Core Data Model
    • Select File -> New -> New file ...
    • Select iOS -> Core Data (On the left panel)
    • Select Data Model (On the right panel)
  • Select Model.xcdatamodeld
  • Create Entity
    • Select Add Entity
    • Enter the Entity Name
    • Select Add Attribute and enter attribute name and type
    • In the Utility Panel, select Data Model Inspector
    • Change the attribute's settings if needed
  • Create model relationship
    • Add + sign under relationship in the editor window
    • Enter the destination
    • Specific whether it is to-many relationship in the Model Inspector

Create Object Class for the Created Data Entities

  • Select Model.xcdatamodeld
  • In the editor, select the Entities of interest
  • Select File -> New -> New File ...
  • Select NSManagedObject subclass

Generate Code

User.h
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@class Phone;

@interface User : NSManagedObject

@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSSet *phones;
@end

@interface User (CoreDataGeneratedAccessors)

- (void)addPhoneObject:(Phone *)value;
- (void)removePhoneObject:(Phone *)value;
- (void)addPhone:(NSSet *)values;
- (void)removePhone:(NSSet *)values;
@end
User.m
#import "User.h"
#import "Phone.h"

@implementation User

@dynamic name;
@dynamic phone;

@end
@dynamic name;
  • Objective C runtime allows application to interview when received an un-defined message call
  • @dynamic allows the compiler to suppress compiler error when an object send an un-defined message in the code
  • NSManagedObject will intervine and serve the un-defined message
  • The following will not create a compilation error even the implementation class does not implement the method
    user.name;
    

Fetch & Create Data

+ (phone *)findOrCreatePhone:(NSDictionary *)data inManagedObjectContext:(NSManagedObjectContext *)context
{
   Phone *phone = nil;

   NSFetchRequest *request = [[NSFetchRequest alloc] init];
   request.entity = [NSEntityDescription entityForName:@"Phone" inManagedObjectContext:context];
   request.predicate = [NSPredicate predicateWithFormat:@"number = %@", [data objectForKey:@"number"]];

   NSError *error = nil;
   phone = [[context executeFetchRequest:request error:&error] lastObject];

   if (!error && !phone) {
      phone = [NSEntityDescription insertNewObjectForEntityForName:@"Phone" inManagedObjectContext:context];
      phone.number = [data objectForKey:@"number"];
      phone.user = [User findOrCreateUser:data inManagedObjectContext:context];
   }

   return phone;
}

Select the name of the entity

   request.entity = [NSEntityDescription entityForName:@"Phone" inManagedObjectContext:context];

Define the condition(s)

   request.predicate = [NSPredicate predicateWithFormat:@"number = %@", [data objectForKey:@"number"]];

Execute the query

   phone = [[context executeFetchRequest:request error:&error] lastObject];

Create a new row.

  • insertNewObjectForEntityForName: only creates a new object
          phone = [NSEntityDescription insertNewObjectForEntityForName:@"Phone" inManagedObjectContext:context];
    
  • A separate API save persist the data to the DB

Another example of finding or creating a object

+ (User *)findOrCreateUser:(NSDictionary *)data inManagedObjectContext:(NSManagedObjectContext *)context
{
   User *user = nil;

   NSFetchRequest *request = [[NSFetchRequest alloc] init];
   request.entity = [NSEntityDescription entityForName:@"User" inManagedObjectContext:context];
   request.predicate = [NSPredicate predicateWithFormat:@"name = %@", [data objectForKey:@"name"]];

   NSError *error = nil;
   user = [[context executeFetchRequest:request error:&error] lastObject];

   if (!error && !user) {
     user = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:context];
     user.name = [data objectForKey:@"name"];
   }

   return user;
}

Application Delegate

Application Delegate header file

#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>

@interface MyAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
@private
    NSManagedObjectContext *_managedObjectContext;
    NSPersistentStoreCoordinator *_persistentStoreCoordinator;
    NSManagedObjectModel *_managedObjectModel_;
}

@property (nonatomic, strong) IBOutlet UIWindow *window;

@property (nonatomic, strong, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, strong, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, strong, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (NSString *)applicationDocumentsDirectory;
- (void)saveContext;

@end

Maintain pointers to the Managed Objects needed in other APIs

@property (nonatomic, strong, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, strong, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, strong, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

Retrieve Data in the Application Delegate

  • Retrieve a list of phone information when the application start
  • Create the phone/user object if it is not in the DB
  • Save it to the DB
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        NSArray *phones = [...]; Retrieve phones/users information
    
        for (NSDictionary *phone in phones) {
    	[Phone findOrCreatePhone::phone	inManagedObjectContext:self.managedObjectContext];
        }
        [self saveContext];
    
        ...
    }
    
  • Persist all changes in the Managed Object Context to the DB
    - (void)saveContext {
        NSError *error = nil;
        if (_managedObjectContext != nil) {
            if ([_managedObjectContext hasChanges] && ![_managedObjectContext save:&error]) {
                // Code to handle the error
                ...
                NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    
            }
        }
    }
    

Application Delegate System Logic

Maintain the Managed Objects in the application delegate that needed in other Core Data APIs

Returns the managed object context

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

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

Return the managed object model

  • NSManagedObjectModel describes a DB schema
  • Create the model if not created
    - (NSManagedObjectModel *)managedObjectModel {
    
        if (_managedObjectModel != nil) {
            return _managedObjectModel;
        }
        NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"Model" ofType:@"momd"];
        NSURL *modelURL = [NSURL fileURLWithPath:modelPath];
        _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
        _return managedObjectModel;
    }
    

NSPersistentStoreCoordinator associate persistent stores with a model

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {

    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }

    NSURL *storeURL = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory]
                                   stringByAppendingPathComponent: @"Model.sqlite"]];

    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
                                        initWithManagedObjectModel:[self managedObjectModel]];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                      configuration:nil URL:storeURL options:nil error:&error]) {
        // Handle the error
        ...
        NSLog(@"Error %@, %@", error, [error userInfo]);
    }

    return _persistentStoreCoordinator;
}

Return the document root directory of the application

- (NSString *)applicationDocumentsDirectory {
    return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
}

Save data when terminate or move to the background

- (void)applicationWillTerminate:(UIApplication *)application {
    [self saveContext];
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
    [self saveContext];
}

Implement View Controllers to Display Core Data

We are going to implement 2 Table View Controllers

  • When the application starts, it lists all the users with the UserTableViewController
  • When a specific user is selected, it navigates to the phone list for that user

Implement a User Table View Controller to display all users

UserTableViewController.h
#import <Foundation/Foundation.h>
#import "BasicTableViewController.h"

@interface UserTableViewController : BasicTableViewController

- initWithMOC:(NSManagedObjectContext *)context;
@end
  • initWithMOC allows the application delegate to pass in a Managed Object Context
UserTableViewController.m
#import "UserTableViewController.h"
#import "PhoneTableViewController.h"

@implementation UserTableViewController

- initWithMOC:(NSManagedObjectContext *)context
{
  if (self = [super initWithStyle:UITableViewStylePlain])
  {
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    request.entity = [NSEntityDescription entityForName:@"User" inManagedObjectContext:context];
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES
                                     selector:@selector(caseInsensitiveCompare:)]];

    request.predicate = nil;
    request.fetchBatchSize = 10;

    NSFetchedResultsController *rc = [[NSFetchedResultsController alloc]
			initWithFetchRequest:request
			managedObjectContext:context
			sectionNameKeyPath:@"firstChar"
	                cacheName:@"MyCache"];

    self.fetchedResultsController = rc;
    self.titleKey = @"name";
    self.searchKey = @"name";
  }
  return self;
}
* Create and configure the NSFetchRequest to fetch all user
* Create the NSFetchedResultsController and set it to self.fetchedResultsController

* Create section when display the name by the first character
{code:title=User.m}
- (NSString *)firstChar
{
	return [[self.name substringToIndex:1] capitalizedString];
}

To set the User Table View Controller as the first screen of the application

  • Inside application delegate's didFinishLaunchingWithOptions:
  • Initialize the UserTableViewController with the Managed Object Context
  • Initialize a navigation controller
  • Push the UserTableViewController to the navigation controller
  • Display the navigation controller
    Application Delegate
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
      ...
    
      UserTableViewController *vc = [[UserTableViewController alloc] initWithMOC:self.managedObjectContext];
      UINavigationController *nav = [[UINavigationController alloc] init];
      [nav pushViewController:vc animated:NO];
    
      [window addSubview:nav.view];
      [window makeKeyAndVisible];
    
      return YES;
    }
    

When a user is selected, push the phone table view onto the navigation controller

  • Initialized the phone table view with the user object
  • Push the phone table view onto the navigation control
    UserTableViewController.m
    - (void)managedObjectSelected:(NSManagedObject *)managedObject
    {
       User *user = (User *)managedObject;
       PhoneTableViewController *vc = [[PhoneTableViewController alloc] initWithUser:user];
       [self.navigationController pushViewController:vc animated:YES];
    }
    @end
    

The second Table View Controller

PhoneTableViewController.h
#import <Foundation/Foundation.h>
#import "BasicTableViewController.h"
#import "User.h"

@interface PhoneTableViewController : BasicTableViewController

- initWithUser:(User *)user;

@end

Phone Table View Controller implementation

PhoneTableViewController.m
#import "PhoneTableViewController.h"
#import "Phone.h"

@implementation PhoneTableViewController

- initWithUser:(User *)user;
{
  if (self = [super initWithStyle:UITableViewStylePlain])
  {
     NSManagedObjectContext *context = user.managedObjectContext;

     NSFetchRequest *request = [[NSFetchRequest alloc] init];
     request.entity = [NSEntityDescription entityForName:@"Phone" inManagedObjectContext:context];
     request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"number" ascending:YES]];
     request.predicate = [NSPredicate predicateWithFormat:@"user = %@", user];
     request.fetchBatchSize = 10;

     NSFetchedResultsController *rc = [[NSFetchedResultsController alloc]
			initWithFetchRequest:request
			managedObjectContext:context
			sectionNameKeyPath:nil
			cacheName:nil];

     self.fetchedResultsController = rc;
     self.titleKey = @"number";
   }
   return self;
}

- (void)managedObjectSelected:(NSManagedObject *)managedObject
{
   ....
}

@end
  • Set cache to nil because request.predicate change what is requested and therefore the result is different everytime and cannot be cached

BasicTableViewController

#import "BasicTableViewController.h"

@implementation BasicTableViewController

@synthesize fetchedResultsController;
@synthesize titleKey, subtitleKey, searchKey;

- (void)createSearchBar
{
	if (self.searchKey.length) {
		if (self.tableView && !self.tableView.tableHeaderView) {
			UISearchBar *searchBar = [[[UISearchBar alloc] init] autorelease];
			[[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
			self.searchDisplayController.searchResultsDelegate = self;
			self.searchDisplayController.searchResultsDataSource = self;
			self.searchDisplayController.delegate = self;
			searchBar.frame = CGRectMake(0, 0, 0, 38);
			self.tableView.tableHeaderView = searchBar;
		}
	} else {
		self.tableView.tableHeaderView = nil;
	}
}

- (void)setSearchKey:(NSString *)aKey
{
	[searchKey release];
	searchKey = [aKey copy];
	[self createSearchBar];
}

- (NSString *)titleKey
{
	if (!titleKey) {
		NSArray *sortDescriptors = [self.fetchedResultsController.fetchRequest sortDescriptors];
		if (sortDescriptors.count) {
			return [[sortDescriptors objectAtIndex:0] key];
		} else {
			return nil;
		}
	} else {
		return titleKey;
	}
}

- (void)performFetchForTableView:(UITableView *)tableView
{
	NSError *error = nil;
	[self.fetchedResultsController performFetch:&error];
	if (error) {
		NSLog(@"[BasicTableViewController performFetchForTableView:] %@ (%@)", [error localizedDescription], [error localizedFailureReason]);
	}
	[tableView reloadData];
}

- (NSFetchedResultsController *)fetchedResultsControllerForTableView:(UITableView *)tableView
{
	if (tableView == self.tableView) {
		if (self.fetchedResultsController.fetchRequest.predicate != normalPredicate) {
			[NSFetchedResultsController deleteCacheWithName:self.fetchedResultsController.cacheName];
			self.fetchedResultsController.fetchRequest.predicate = normalPredicate;
			[self performFetchForTableView:tableView];
		}
		[currentSearchText release];
		currentSearchText = nil;
	} else if ((tableView == self.searchDisplayController.searchResultsTableView) && self.searchKey && ![currentSearchText isEqual:self.searchDisplayController.searchBar.text]) {
		[currentSearchText release];
		currentSearchText = [self.searchDisplayController.searchBar.text copy];
		NSString *searchPredicateFormat = [NSString stringWithFormat:@"%@ contains[c] %@", self.searchKey, @"%@"];
		NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:searchPredicateFormat, self.searchDisplayController.searchBar.text];
		[NSFetchedResultsController deleteCacheWithName:self.fetchedResultsController.cacheName];
		self.fetchedResultsController.fetchRequest.predicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:searchPredicate, normalPredicate , nil]];
		[self performFetchForTableView:tableView];
	}
	return self.fetchedResultsController;
}

- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller
{
	// reset the fetch controller for the main (non-searching) table view
	[self fetchedResultsControllerForTableView:self.tableView];
}

- (void)setFetchedResultsController:(NSFetchedResultsController *)controller
{
	fetchedResultsController.delegate = nil;
	[fetchedResultsController release];
	fetchedResultsController = [controller retain];
	controller.delegate = self;
	normalPredicate = [self.fetchedResultsController.fetchRequest.predicate retain];
	if (!self.title) self.title = controller.fetchRequest.entity.name;
	if (self.view.window) [self performFetchForTableView:self.tableView];
}

- (UITableViewCellAccessoryType)accessoryTypeForManagedObject:(NSManagedObject *)managedObject
{
	return UITableViewCellAccessoryDisclosureIndicator;
}

- (UIImage *)thumbnailImageForManagedObject:(NSManagedObject *)managedObject
{
	return nil;
}

- (void)configureCell:(UITableViewCell *)cell forManagedObject:(NSManagedObject *)managedObject
{
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForManagedObject:(NSManagedObject *)managedObject
{
    static NSString *ReuseIdentifier = @"BasicTableViewCell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier];
    if (cell == nil) {
		UITableViewCellStyle cellStyle = self.subtitleKey ? UITableViewCellStyleSubtitle : UITableViewCellStyleDefault;
        cell = [[[UITableViewCell alloc] initWithStyle:cellStyle reuseIdentifier:ReuseIdentifier] autorelease];
    }

	if (self.titleKey) cell.textLabel.text = [managedObject valueForKey:self.titleKey];
	if (self.subtitleKey) cell.detailTextLabel.text = [managedObject valueForKey:self.subtitleKey];
	cell.accessoryType = [self accessoryTypeForManagedObject:managedObject];
	UIImage *thumbnail = [self thumbnailImageForManagedObject:managedObject];
	if (thumbnail) cell.imageView.image = thumbnail;

	return cell;
}

- (void)managedObjectSelected:(NSManagedObject *)managedObject
{
    // Navigation logic may go here. Create and push another view controller.
    // AnotherViewController *anotherViewController = [[AnotherViewController alloc] initWithNibName:@"AnotherView" bundle:nil];
    // [self.navigationController pushViewController:anotherViewController];
    // [anotherViewController release];
}

- (void)deleteManagedObject:(NSManagedObject *)managedObject
{
}

- (BOOL)canDeleteManagedObject:(NSManagedObject *)managedObject
{
	return NO;
}

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
	NSManagedObject *managedObject = [[self fetchedResultsControllerForTableView:tableView] objectAtIndexPath:indexPath];
	return [self canDeleteManagedObject:managedObject];
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
	NSManagedObject *managedObject = [[self fetchedResultsControllerForTableView:tableView] objectAtIndexPath:indexPath];
	[self deleteManagedObject:managedObject];
}

#pragma mark UIViewController methods

- (void)viewDidLoad
{
	[self createSearchBar];
}

- (void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear:animated];
	[self performFetchForTableView:self.tableView];
}

#pragma mark UITableViewDataSource methods

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

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
	return [[self fetchedResultsControllerForTableView:tableView] sectionIndexTitles];
}

#pragma mark UITableViewDelegate methods

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[[[self fetchedResultsControllerForTableView:tableView] sections] objectAtIndex:section] numberOfObjects];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	return [self tableView:tableView cellForManagedObject:[[self fetchedResultsControllerForTableView:tableView] objectAtIndexPath:indexPath]];
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	[self managedObjectSelected:[[self fetchedResultsControllerForTableView:tableView] objectAtIndexPath:indexPath]];
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
	return [[[[self fetchedResultsControllerForTableView:tableView] sections] objectAtIndex:section] name];
}

- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
	return [[self fetchedResultsControllerForTableView:tableView] sectionForSectionIndexTitle:title atIndex:index];
}

#pragma mark NSFetchedResultsControllerDelegate methods

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

- (void)controller:(NSFetchedResultsController *)controller
  didChangeSection:(id <NSFetchedResultsSectionInfo>)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:
			[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            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];
}

#pragma mark dealloc

- (void)dealloc
{
	fetchedResultsController.delegate = nil;
	[fetchedResultsController release];
	[searchKey release];
	[titleKey release];
	[currentSearchText release];
	[normalPredicate release];
    [super dealloc];
}

@end

iOS Core Data Programming

To insert a new row

NSManagedObject *user =
   [NSEntityDescription insertNewObjectForEntityForName:@"User"
                         inManagedObjectContext:(NSManagedObjectContext *)ctxt];

To access a column with the MSManagedObject

- (id)valueForKey:(NSString *)key;
- (void)setValue:(id)value forKey:(NSString *)key;