How to populate CoreData at runtime from a remote source.

To me it seems that documentation on populating apple Core Data based stores is sorely lacking instructions on how to populate the database at runtime from remote sources. Most people electing to populate an SQL Lite database and then just copy it in when the app is first loaded.  UGH.  I wanted to retrieve the initial database at runtime and then also use this remote store to update my locally held database at the users request; merging the data intelligently.  The problem was trying to do it as the app launches and trying to do it at various points in the App Delegate or view controller startup screwed with stuff and caused EXC_BAD_ACCESS.  Which implies something somewhere got de-allocated/allocated when it shouldn’t have.  Here is the solution, the remote store is a plist or PropertyList, which is an xml format used a lot by apple and apple software.

I’ll assume a lot of prior knowledge here unless people comment saying they want more information.

Start a new project using core data, in my case it’s a universal iPhone Navigation Bar project.  Within this project I deleted the default ‘Event’ database stuff it set up and created a database and relationships of my own.  In this example I only use one table called ‘Category’ which holds nothing but ‘name’.  A custom cell was created to display this information which also has a UIImageView field loading a local image based on the name of the category.  References in Root View Controller were changed from ‘Event’ to ‘Category’ and from ‘timestamp’ to ‘name’.   Ignoring the code to display the custom cell itself it necessitated edits to:

- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- (NSFetchedResultsController *)fetchedResultsController

I’m not allowing editing here so insertNewObject is gone, but if you were you’d have to edit that to so you can move away from the example data.

The problem for me was not being able to ascertain where best to shove the following code to populate the database:


	// Read in property list
	NSPropertyListFormat *format;
	NSString *errorDesc;
	NSData *plistXML = [[NSData alloc] initWithContentsOfURL:source];
 
	NSDictionary *data = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:plistXML 
																				   mutabilityOption:NSPropertyListImmutable format:format errorDescription:&errorDesc];
	
	NSMutableSet *scategories = [[NSMutableSet alloc] init];
 
	// Get all category names into mutable array
	for(id object in data) {
		for(id key in object) {
			if([key isEqualToString:@"type"])
				[scategories addObject:(NSString *)[object objectForKey:key]];
		}
	}
 
	NSMutableArray *categories = [[NSMutableArray alloc] initWithSet:scategories];
	[scategories release];
	[categories sortUsingSelector:@selector(compare:)];
 
	// Create fetch request to get all category names
	NSFetchRequest *categoriesRequest = [[[NSFetchRequest alloc] init] autorelease];
	[categoriesRequest setEntity:[NSEntityDescription entityForName:@"Category" inManagedObjectContext:managedObjectContext_]];
	[categoriesRequest setPredicate:[NSPredicate predicateWithFormat:@"(name in %@)",categories]];
 
	// Sort our data store categories too like
	[categoriesRequest setSortDescriptors:[NSArray arrayWithObject:[[[NSSortDescriptor alloc] 
																	 initWithKey:@"name" ascending:YES] autorelease]]];
 
	// Execute fetch
	NSError *error;
	NSArray *categoriesMatchingNames = [managedObjectContext_ executeFetchRequest:categoriesRequest error:&error];
 
	// We need to enumarate the categoriesMatchingNames array as we must control when next to fetch an object
	NSEnumerator *enumurateDSCategories = [categoriesMatchingNames objectEnumerator];
 
	// Get the names to parse
	for(NSString *type in categories) {
		Category *dsCategory = (Category *)[enumurateDSCategories nextObject];
		if( (dsCategory == nil) && ![type isEqualToString:[dsCategory name]] ) {
			Category *category = (Category *)[NSEntityDescription insertNewObjectForEntityForName:@"Category" inManagedObjectContext:managedObjectContext_];
			[category setName:type];
		}
	}
 
	if(![managedObjectContext_ save:&error])
	{	
	 NSLog(@"Core data failed to save new objects!");
	}
	
	[categories release];
	[plistXML release];
	
	return TRUE;

I tried placing this in:


- (NSPersistentStoreCoordinator *)persistentStoreCoordinator 

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

and viewDidLoad, viewDidAppear and others within the Root View Controller. None worked, they populated the database but then crashed. I finally decided that the best approach would be to argue that if I had no categories defined the other data isn’t there either. To begin with I settled on a modal dialogue but your approach could vary. In the App Delegate I defined two functions, one to do the actual work later on, not yet implemented except for Category, and the other to use a local xml file initially anyway:


- (BOOL)importData {
	NSString *filePath = [[NSBundle mainBundle] pathForResource:@"dl" ofType:@"xml"];  
	NSURL *localfile = [[NSURL alloc] initFileURLWithPath:filePath];
	[self mergeDataFromExternalSource:localfile];
	return TRUE;
}

- (BOOL)mergeDataFromExternalSource:(NSURL *)source {
	// Read in property list
	NSPropertyListFormat *format;
	NSString *errorDesc;
	NSData *plistXML = [[NSData alloc] initWithContentsOfURL:source];
 
	// Get entire XML file into an NSDictionary object which is ideal for plist data
	NSDictionary *data = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:plistXML 
							   mutabilityOption:NSPropertyListImmutable format:format errorDescription:&errorDesc];
	
	// Use NSMutableSet as we are parsing the plist and we only want one instance of each category
	// A set does that for us unlike an array.
	NSMutableSet *scategories = [[NSMutableSet alloc] init];
 
	// Get all category names into mutable array
	for(id object in data) {
		for(id key in object) {
			if([key isEqualToString:@"type"])
				[scategories addObject:(NSString *)[object objectForKey:key]];
		}
	}
 
	// However we cannot sort sets so we now change it into an array anyway
	NSMutableArray *categories = [[NSMutableArray alloc] initWithSet:scategories];
	[scategories release];
	[categories sortUsingSelector:@selector(compare:)];
 
	// Create fetch request to get all category names
	NSFetchRequest *categoriesRequest = [[[NSFetchRequest alloc] init] autorelease];
	[categoriesRequest setEntity:[NSEntityDescription entityForName:@"Category" inManagedObjectContext:managedObjectContext_]];
	// Only fetch records that exist in our input data
	[categoriesRequest setPredicate:[NSPredicate predicateWithFormat:@"(name in %@)",categories]];
 
	// Sort our data store categories too so they match our input data.
	[categoriesRequest setSortDescriptors:[NSArray arrayWithObject:[[[NSSortDescriptor alloc] 
												 initWithKey:@"name" ascending:YES] autorelease]]];
 
	// Execute fetch
	NSError *error;
	NSArray *categoriesMatchingNames = [managedObjectContext_ executeFetchRequest:categoriesRequest error:&error];
 
	// We need to enumarate the categoriesMatchingNames array as we must control when next to fetch an object
	// We only want to fetch the next object when it matches a category as we have n input categories but potential n-x 
	// stored ones.
	NSEnumerator *enumurateDSCategories = [categoriesMatchingNames objectEnumerator];
 
	// Get an initial category
	Category *dsCategory = (Category *)[enumateDSCategories nextObject];
	// Get the names to parse
	for(NSString *type in categories) {
		// If the data store category is blank (we had none or run out) or they don't match add it.
		if( (dsCategory == nil) || ![type isEqualToString:[dsCategory name]] ) {
			Category *category = (Category *)[NSEntityDescription insertNewObjectForEntityForName:@"Category" inManagedObjectContext:managedObjectContext_];
			[category setName:type];
		}
		// If the data store category matches the input one get the next data store category
		if( [type isEqualToString:[dsCategory name]] ) {
			Category *dsCategory = (Category *)[enumurateDSCategories nextObject];
		}
	}
 
	if(![managedObjectContext_ save:&error])
	{	
	 NSLog(@"Core data failed to save new objects!");
	}
	
	[categories release];
	[plistXML release];
	
	return TRUE;
}

in the Root View Controller source file (RootViewController.m) the following is modified/added:


- (void)viewDidAppear:(BOOL)animated {
	id  sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:0];
	if ( [sectionInfo numberOfObjects] < 1) {
		UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"No Data." 
														message:@"Initial data not found, loading defaults.\nHit cancel  and quit to configure a remote source in Settings.app."
													   delegate:self
											  cancelButtonTitle:@"Cancel" 
											  otherButtonTitles:nil];
		[alert addButtonWithTitle:@"OK"];
		[alert show];
		[alert release];
	}
	
    [super viewDidAppear:animated];
}

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
	if ( buttonIndex == 1 )
		[((Delicious_LibraryAppDelegate *)[[UIApplication sharedApplication] delegate]) importData];
}

The above counts how many rows I have in my main table (which has one column, hence objectAtIndex is 0) and if it is less than 1 I assume we have no data and prompt the user. When the user hits OK the importData function is called. For now it just loads the local data but it could check the app settings and use the remote function.

This entry was posted in Programming and tagged , , , , . Bookmark the permalink.

2 Responses to How to populate CoreData at runtime from a remote source.

Comments are closed.