Ad

Our DNA is written in Swift
Jump

Creating a CoreData Model in Code

I’m working on my own file and image cache that uses CoreData for storage. The same way that NSURLCache is doing it, but with some optimizations that I know and understand. So I created DTDownloadCache and got it all working, but there was one minor thing that I didn’t like: The usual method of creating a CoreData entity model is by the entity editor built into Xcode.

This meant that I had to include the .datamodeld file in apps using this. But I didn’t want to have to create a resource bundle just for this single file as you would have to do if you keep your reusable code in static libraries. Ugh!

Fortunately there is a way how you can create a static model entirely in code so that you can make use of CoreData without having to ship an XML description of the model.

First, lets have a quick look at how the model looked like in the editor.

Plain and simple. The only special thing is to allow external storage for the fileData attribute. This allows CoreData to decide where to most efficiently store the file data. Small files will be stored in the entity itself. Large files get a GUID and will be saved in a subfolder of Library/Caches. If we wanted to support iOS 4 then we would have to create this mechanism ourselves which is available as of iOS 5.

In XML this is represented as:

The model is what CoreData needs to know how to translate between the SQLite database on disk and the managed objects in memory. Of course we could embed this model as a binary resource, but that would make out code unnecessarily more convoluted. I found a good example (Core Data Utility Tutorial) in the Apple documentation that explains the process.

The method to create the same model as in the editor in code looks like this. ARC = On.

- (NSManagedObjectModel *)_model
{
	NSManagedObjectModel *model = [[NSManagedObjectModel alloc] init];
 
	// create the entity
	NSEntityDescription *entity = [[NSEntityDescription alloc] init];
	[entity setName:@"DTCachedFile"];
	[entity setManagedObjectClassName:@"DTCachedFile"];
 
	// create the attributes
	NSMutableArray *properties = [NSMutableArray array];
 
	NSAttributeDescription *remoteURLAttribute = [[NSAttributeDescription alloc] init];
	[remoteURLAttribute setName:@"remoteURL"];
	[remoteURLAttribute setAttributeType:NSStringAttributeType];
	[remoteURLAttribute setOptional:NO];
	[remoteURLAttribute setIndexed:YES];
	[properties addObject:remoteURLAttribute];
 
	NSAttributeDescription *fileDataAttribute = [[NSAttributeDescription alloc] init];
	[fileDataAttribute setName:@"fileData"];
	[fileDataAttribute setAttributeType:NSBinaryDataAttributeType];
	[fileDataAttribute setOptional:NO];
	[fileDataAttribute setAllowsExternalBinaryDataStorage:YES];
	[properties addObject:fileDataAttribute];
 
	NSAttributeDescription *lastAccessDateAttribute = [[NSAttributeDescription alloc] init];
	[lastAccessDateAttribute setName:@"lastAccessDate"];
	[lastAccessDateAttribute setAttributeType:NSDateAttributeType];
	[lastAccessDateAttribute setOptional:NO];
	[properties addObject:lastAccessDateAttribute];
 
	NSAttributeDescription *expirationDateAttribute = [[NSAttributeDescription alloc] init];
	[expirationDateAttribute setName:@"expirationDate"];
	[expirationDateAttribute setAttributeType:NSDateAttributeType];
	[expirationDateAttribute setOptional:NO];
	[properties addObject:expirationDateAttribute];
 
	NSAttributeDescription *contentTypeAttribute = [[NSAttributeDescription alloc] init];
	[contentTypeAttribute setName:@"contentType"];
	[contentTypeAttribute setAttributeType:NSStringAttributeType];
	[contentTypeAttribute setOptional:YES];
	[properties addObject:contentTypeAttribute];
 
	NSAttributeDescription *fileSizeAttribute = [[NSAttributeDescription alloc] init];
	[fileSizeAttribute setName:@"fileSize"];
	[fileSizeAttribute setAttributeType:NSInteger32AttributeType];
	[fileSizeAttribute setOptional:NO];
	[properties addObject:fileSizeAttribute];
 
	NSAttributeDescription *entityTagIdentifierAttribute = [[NSAttributeDescription alloc] init];
	[entityTagIdentifierAttribute setName:@"entityTagIdentifier"];
	[entityTagIdentifierAttribute setAttributeType:NSStringAttributeType];
	[entityTagIdentifierAttribute setOptional:YES];
	[properties addObject:entityTagIdentifierAttribute];
 
	// add attributes to entity
	[entity setProperties:properties];
 
	// add entity to model
	[model setEntities:[NSArray arrayWithObject:entity]];
 
	return model;
}

This looks more complicated than it really is because of the verbosity of Objective-C. The process is simply to create an empty model, create an entity, create the attributes, stir it all up and cook on medium flame for 15 minutes. If you compare this with the XML model above then you’ll see a couple of differences, but those are merely because I made my code model more strict, with less optional properties.

The bare bones CoreData stack – consisting of the model, the persistent store coordinator and the managed object context might look like this:

- (void)_setupCoreDataStack
{
	// setup managed object model
 
	/*
	NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"DTDownloadCache" withExtension:@"momd"];
	_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
	 */
 
	// in code
	_managedObjectModel = [self _model]; 
 
	// setup persistent store coordinator
	NSURL *storeURL = [NSURL fileURLWithPath:[[NSString cachesPath] stringByAppendingPathComponent:@"DTDownload.cache"]];
 
	NSError *error = nil;
	_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_managedObjectModel];
 
	if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) 
	{
		// inconsistent model/store
		[[NSFileManager defaultManager] removeItemAtURL:storeURL error:NULL];
 
		// retry once
		if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) 
		{
			NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
			abort();
		}
	}
 
	// create MOC
	_managedObjectContext = [[NSManagedObjectContext alloc] init];
	[_managedObjectContext setPersistentStoreCoordinator:_persistentStoreCoordinator];
}

Xcode turns the XML model into a momd file when building. The line that loads the model from this file is now commented out and instead the model is being constructed at runtime.

You can modify the model until the first time it is used to access the persistent store. After that time it is read-only because changing it then would make it impossible for CoreData to talk to the data it has already saved.

Also this approach does not bother with migrating models if something is changed. We simply remove the store file and retry adding the persistent store to the store coordinator. If that works then CoreData has created a fresh SQLite database on disk that fits our new model.

Conclusion

CoreData is powerful to be used for maintaining the META information of caches and with the above approach you can easily keep your custom caches in static libraries without the need of adding a clunky model file to apps that are linking in this lib.


Categories: Recipes

8 Comments »

  1. Hi, i am using external storage, and i have encounter a problem when migrating to a new version of the model. All the external data is missing. Have you tried migrating? Thank you!

  2. Sounds like an iOS bug, file a Radar. I never used migration until now.

  3. Thanks for this. Could you extend the example to include relationships?