3
May
2009
 

Core Data and Plug-ins

by Marcus Zarra

Thanks to the ability to have configurations in a Core Data Managed Object Model and being able to save data to multiple Persistent Stores, it is possible to have a Core Data Model that is constructed from not only an internal model, but from the models of all the plug-ins that are loaded into the application.

In this example we are going to build a basic application with the following requirements:

  • A plug-in framework
  • Plug-ins can extend the managed object model of the application
  • Removal of a plug-in should not corrupt the persistent store.

The Concept

Core Data allows a NSManagedObjectModel to be constructed from multiple “sub” models. Therefore we can load up all of our plug-ins and ask them for their NSManagedObjectModel references. Using those plus the models included with the application we can build a composite model.

However this would still save into a single file which would then become fragile if a plug-in disappeared. Instead we will use Core Data Configurations. By declaring a configuration for each model, we can specify a different file on disk for each configuration and thereby be able to split the persistent store by model on disk. If a plug-in disappears, the configuration is not loaded and its corresponding persistent store is not loaded and therefore the integrity of the persistent store stays intact.

The Plug-in Framework

In this example we are not going to explore Plug-in design in too much depth. That subject has been covered elsewhere. Our plug-in for this design is going to use a framework to host the shared code and both the application and the bundles will link to it and import from it.

Our framework is going to consist of a header file that defines the protocol that the plug-ins must implement to be loaded. The header is as follows:

@protocol ZSPlugin 

- (NSString*)name;
- (NSString*)modelConfigurationName;
- (NSManagedObjectModel*)managedObjectModel;

@end

The Example Plug-in

To test this we need to include at least one plug-in. That plug-in will consist of a principal class along with a data model. The plug-in does not need to stand up a Core Data stack because its model will be included in the primary application’s Core Data stack. Therefore we just need to implement the methods in the protocol.

-name

The name method in this example just returns a string.

- (NSString*)name;
{
  return @"Example Plugin v1.0";
}

-modelConfigurationName

Like the name method above, the -modelConfigurationName method only returns a string. In a more robust solution we would check in the plug-in manager to confirm that this name is unique and does not conflict with the configuration of the base application.

- (NSString*)modelConfigurationName;
{
  return @"ExamplePlugin";
}

-managedObjectModel

The final method that we declare in the protocol returns the NSManagedObjectModel for the plug-in. This method does a simple load from the plug-ins bundle.

- (NSManagedObjectModel*)managedObjectModel;
{
  if (managedObjectModel) return managedObjectModel;

  NSBundle *myBundle = [NSBundle bundleForClass:[self class]];
  NSArray *bundles = [NSArray arrayWithObject:myBundle];
  managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:bundles] retain];
  return managedObjectModel;
}

The one interesting part of this code is that we need to get a reference to the NSBundle for the plug-in from its principal class. This is because the call [NSBundle mainBundle] will return the application’s bundle instead.

The Plug-in Manager

The plug-in manager loads all of the available plug-ins on launch. In this example we do not allow dynamic plug-in loading.

#import 

@interface ZSPluginManager : NSObject 
{
  NSArray *loadedPlugins;
}

@property (retain) NSArray *loadedPlugins;

+ (id)shared;

- (NSArray*)pluginModels;
- (NSArray*)modelConfigurations;
- (NSString*)applicationSupportFolder;

@end

-init

The plug-in manager is a singleton that initializes itself and loads all of the existing plug-ins upon first request.

- (id)init
{
  if (!(self = [super init])) return nil;

  //Find the plugins
  NSFileManager *fileManager = [NSFileManager defaultManager];
  NSArray *plugins = [fileManager directoryContentsAtPath:[self applicationSupportFolder]];

  if (![plugins count]) {
    NSLog(@"%@:%s No plugins found", [self class], _cmd);
    return self;
  }

  //Load all of the plugins
  NSMutableArray *loadArray = [NSMutableArray array];
  for (NSString *pluginPath in plugins) {
    if (![pluginPath hasSuffix:@".bundle"]) continue;
    NSBundle *pluginBundle = [NSBundle bundleWithPath:pluginPath];
    Class principalClass = [pluginBundle principalClass];
    if (![principalClass conformsToProtocol:@protocol(ZSPlugin)]) {
      NSLog(@"Invalid plug-in, does not conform to the ZSPlugin Protocol: %@", pluginPath);
      continue;
    }
    id plugin = [[principalClass alloc] init];
    [loadArray addObject:plugin];
    NSLog(@"Plug-in Loaded: %@", [plugin name]);
    [plugin release], plugin = nil;
  }
  [self setLoadedPlugins:loadArray];

  return self;
}

In the -init method we first find all of the files in the Application Support directory. If there are no files then we quickly return self. If there are files then we start looping over them. On each iteration we check to see if the file is a plug-in and if it is not we skip to the next loop. If it is then we look-up its principal class and confirm that it conforms to the ZSPlugin protocol. If it does not we warn the developer and skip to the next loop.

Once we pass all of the integrity checks we then call alloc and init on the plug-in and add it to the array of loaded plug-ins.

-pluginModels

To help the main application initialize we have a couple of helper methods in the plug-in manager. The first is a method that returns all of the plug-in models.

- (NSArray*)pluginModels;
{
  NSMutableArray *array = [NSMutableArray array];
  for (id plugin in [self loadedPlugins]) {
    [array addObject:[plugin managedObjectModel]];
  }
  return array;
}

-modelConfigurations

The second helper method returns an NSArray of the configuration names used by the plug-ins. These names will be used both to load the configurations and to decide on the persistent store’s file name.

- (NSArray*)modelConfigurations;
{
  NSMutableArray *array = [NSMutableArray array];
  for (id plugin in [self loadedPlugins]) {
    [array addObject:[plugin modelConfigurationName]];
  }
  return array;
}

The Primary Application

With our quick walkthrough of the plug-in structure complete we need to review the changes to the application itself. All of the changes are limited to the Core Data methods.

-managedObjectModel

The first change we need to make is how we load the NSManagedObjectModel. Normally we would just call [NSManagedObjectModel mergedModelFromBundles:nil] and be done. However we want to load not only the models within the application itself but also merge with all of the models in the plug-ins. Therefore a couple of extra steps are required.

- (NSManagedObjectModel*)managedObjectModel 
{
  if (managedObjectModel) return managedObjectModel;

  NSMutableArray *models = [NSMutableArray array];
  [models addObject:[NSManagedObjectModel mergedModelFromBundles:nil]];
  [models addObjectsFromArray:[[ZSPluginManager shared] pluginModels]];

  managedObjectModel = [[NSManagedObjectModel modelByMergingModels:models] retain];
  return managedObjectModel;
}

In this method we start with the NSManagedObjectModel from the Application itself and add it to a NSMutableArray. We then request an array of all the models from the plug-ins via the ZSPluginManager and add those models to the NSMutableArray. Once we have all of the models together we call +modelByMergingModels: and merge all of the models into one super model. We retain that model and return it to the caller.

-persistentStoreCoordinator

The second and last change we need to make to the Core Data stack is the way that we handle the NSPersistentStoreCoordinator.

- (NSPersistentStoreCoordinator*)persistentStoreCoordinator 
{
  if (persistentStoreCoordinator) return persistentStoreCoordinator;

  NSFileManager *fileManager;
  NSString *applicationSupportFolder = nil;
  NSURL *url = nil;
  NSError *error = nil;
  NSString *filePath = nil;

  fileManager = [NSFileManager defaultManager];
  applicationSupportFolder = [[ZSPluginManager shared] applicationSupportFolder];
  if (![fileManager fileExistsAtPath:applicationSupportFolder isDirectory:NULL] ) {
    if (![fileManager createDirectoryAtPath:applicationSupportFolder attributes:nil]) {
      NSLog(@"%@:%s Failed to create app support directory", [self class], _cmd);
      return nil;
    }
  }

  NSManagedObjectModel *mom = [self managedObjectModel];
  if (!mom) return nil;
  NSPersistentStoreCoordinator *psc = nil;
  psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];

  NSMutableArray *configArray = [NSMutableArray array];
  [configArray addObject:@"Core"];
  [configArray addObjectsFromArray:[[ZSPluginManager shared] modelConfigurations]];

  for (NSString*configName in [[ZSPluginManager shared] loadedPlugins]) {
    filePath = [configName stringByAppendingPathExtension:@"sqlite"];
    filePath = [applicationSupportFolder stringByAppendingPathComponent:filePath];
    url = [NSURL fileURLWithPath:filePath];
    if (![psc addPersistentStoreWithType:NSSQLiteStoreType 
                           configuration:configName 
                                     URL:url 
                                 options:nil 
                                   error:&error]) {

      [[NSApplication sharedApplication] presentError:error];
      [psc release], psc = nil;
      return nil;
    }
  }

  persistentStoreCoordinator = psc;
  return persistentStoreCoordinator;
}

The first half of the method is almost straight out of the template. However once we have the application support directory created and the raw NSPersistentStoreCoordinator initialized it is time to deviate.

The first then we do is construct a NSMutableArray and add our application model’s configuration name to it. In this example I called it “Core”. With the array initialized we then grab all of the configuration names from the plug-ins via the ZSPluginManager and add them to the array.

With the array fully populated it is time to iterate over it. Within each iteration we do the following:

  1. We construct a file path for the configuration using the application support folder and the configuration name. We add a “sqlite” extension to it although in a production system you should use an application specific extension.

  2. We then add the newly constructed file path to the NSPersistentStoreCoordinator re-using the configuration name and checking for failure. If we fail we present an error and abort.

Conclusion

That is all there is to it! Core Data takes over from there and automatically saves the correct objects into the correct store files for us. If a plug-in goes away we don’t load its model and we don’t reference it’s store so integrity remains.

In the included example application I have also built a UI which lists the entities that are available in the model. If you run it straight from the zip file you will see the following UI:

However, if after building the application, you copy the example plugin into your ~/Library/Application Support/CDPlugins directory and run the application again you will see the following.

Which shows that not only is the plug-in loaded but that it’s entity (Widget) has been included in the Core Data stack. This can even be extended to create widgets and then remove the plug-in to confirm that integrity is maintained.

xcode.png
Plug-in Demo Project

Comments

f93erad says:

Nice!

The next step would be to add support for document-based Core Data apps. Storing one additional file for each plug-in would be slightly impractical for end users. One would probably want to place all files together in a bundle instead.

Before reading this article I would have said that extra plug-in storage in a document-based Core Data app is incredibly difficult. Now it just seems like a lot of hard work!

omnius says:

What if entities in one model are subentities of an abstract class defined in the main model?

I tried making a duplicate abstract entity in the second core data model, but it still crashed complaining of duplicate entities. Without the duplicate, there seems to be no way to tell core data that the entity is actually a subentity of an entity in a different file.

Marcus Zarra says:

You can’t do that. Each model is a silo of data that can only have weak references to the data in the other silos.

Gregory Hill says:

Markus, I think you’ve answered a prayer of mine. Thanks so much for this post.

Do you have any posts/articles that dig further into plugins themselves? In briefly reading through this, it looks like you might be talking about static frameworks, but I haven’t read closely enough to be sure. If you have any more detailed discussions (or can point me towards someone who does), I’d love a link.

Quick aside–I live in the Baltimore area, and there’s an Airline (AirTran?) that is running a series of commercials related to the Baltimore Ravens. One of them features a rather … intense … individual who knows trivia down to the minute detail on his fantasy football players. His friend suggests he needs a girlfriend. He responds (maybe a slight paraphrase): “Fantasy football is my girlfriend.”

Thought you’d appreciate it. ;-)

-greg

Marcus Zarra says:

While I do not have any other articles on plug-ins, they are fairly well documented by Apple. A plug-in can be a static library, game data, or really anything. In the design I discuss above they are static libraries but that is definitely not a requirement.

Gregory Hill says:

K. I’m not getting this to work. I built the example, then created the “CDPlugins” folder under “Application Support” (I’m assuming I needed to do that, because it wasn’t there). I then first dropped the ExamplePlugin folder into that folder, and then just put the files in the CDPlugins folder.

Neither way produced the expected results.

Any ideas? I really want to get this to work, as this technique will save me an enormous amount of potential heart-ache. I’ve got a huge CD data model, and need to use it in two different apps. If I can get this to work, then I don’t have to keep copying changes over from one to the other!

Also. I’m thinking this through, and I don’t quite see how this will work for Cocoa Touch apps. Am I supposed to drop the plugin into one of the sub-folders under “iPhone Simulator”?

Sorry if I’m being dense about this. I’m definitely in new territory, here.

-greg

Marcus Zarra says:

First, you can’t really have plug-ins on iOS as you are not allowed to download and run code.

Second, this article was designed for the desktop.

Third, if you need to share a model between two projects, just share it. Multiple Xcode projects can reference the same file with no ill effect.

Gregory Hill says:

Okay. You’re using English words, but I’m still not quite following. :-)

When you say “just share it” with respect to models between two projects, I’m not sure I understand what that means or how to do it. How does this work?

Sorry for the neophyte-like questions. I’m good at some stuff, but I’m just not quite groking what you’re saying.

In the meantime, I’ll try Google for a thousand dollars…. ;-) Maybe I’ll get a burst of enlightenment.

-greg

Marcus Zarra says:

Just share it as in include it in both projects. Multiple Xcode projects can link to the same file(s) and use them. So in your case you can reference the same model from both projects and whenever you change the model, both projects can see it.

Gregory Hill says:

Catching back up. Thanks for the response.

Now I’m seeing what you’re saying. I’ll have to experiment with this when I have some down-time. I’m going into beta-test mode with some folks, and I think now’s not the time to be mucking around. :-)

I’ll let you know what comes of my travails.

-g

ck says:

Thanks for your post … I still had some problems getting it to run. After making some changes to the code it seems to work now, though.

I also added some code that creates some actual entities. Doing this I ran into some other problem that made me wonder if you know how to sort that.

If I run the CDPlugins application the first time without the plugin and create a Person entity everything is fine and gets successfully saved to the store.

Adding the plugin to the application support folder and launching the application again results in the following error message when trying to save a Widget entity to the store.

‘The managed object model version used to open the persistent store is incompatible with the one that was used to create the persistent store.’

The updated source can be found at:
http://dl.dropbox.com/u/706599/CDPlugins.zip

Marcus Zarra says:

If you are getting a version error like that then I would look close at your code and make sure that each plugin is creating its own store file on disk as opposed to modifying the base store used by the application.