Chinaunix首页 | 论坛 | 博客
  • 博客访问: 894831
  • 博文数量: 322
  • 博客积分: 6688
  • 博客等级: 准将
  • 技术积分: 3626
  • 用 户 组: 普通用户
  • 注册时间: 2010-09-19 11:26
文章分类

全部博文(322)

文章存档

2013年(5)

2012年(66)

2011年(87)

2010年(164)

分类: BSD

2012-03-30 09:55:49

Like most developers, I look to Apple’s default application templates to get up-to-speed on what would appear as being the Right Way™ of developing apps on iOS. In practice however what you need to realize is Apple’s templates are meant to be the easiest introduction to a set of tools that can be fairly complicated for beginners to understand.  Core Data is one of those areas. The problem is when you try to grow your application you’ve built on top of Apple’s sample template. You’ll experience some annoying growing pains, and will need to give your code a thorough washing and a fresh coat of wax to be able to mature your application.

In my code I’ve learned to share and reuse my classes with other applications I’m writing by encapsulating a lot of the boilerplate into reusable classes, as well as wrapping my whole Core Data model in a reusable static library. This wasn’t the most intuitive thing to get right, but now that it’s done it was really worth the effort. Let me show you how it’s done.

Note: All code samples here will be for iOS4 and above and have been formatted to fit this blog post. For the sake of brevity I use blocks to make the Singleton examples easier; this is possible in iOS3.x, but it’s much more verbose.

Before I dive into any details I wanted to start with a bold statement:

Apple’s sample Core Data templates suck.

That isn’t to say that it isn’t valuable for Core Data beginners to get started, but it isn’t a good idea to use them verbatim as an optimal approach for designing an application around Core Data. This is due to the following reasons:

Your UIApplicationDelegate class should be there for one thing only: responding to application’s state changes. Overloading it to manage your Core Data stack is the wrong place to do things.

There are certain database-related operations that should be called from within your application delegate, such as performing a “save” when the application is about to terminate, but those are fairly minor cases that shouldn’t drive the entire design of your application.

The way the NSManagedObjectModel is constructed in Apple’s templates assumes you only have one version of your object model. You’ll have to change this code around when you add another version later in the life of your application. You might as well start on the right foot and assume your application will mature and eventually grow into a second version of its object model.

The easiest way to create an NSManagedObjectModel, and the way it works in Apple’s templates, is with the following call:

1
[NSManagedObjectModel modelByMergingModels:nil];

This essentially tells Core Data to take all available object models included with your application’s [NSBundle mainBundle] and merges them together into one contiguous view. This blows up in your face however if you have multiple versions of the same object model, or if your managed object model lives somewhere other than your main bundle.

Instead you can construct your managed object model like so:

1
2
3
4
5
6
7
8
NSManagedObjectModel *objModel;
NSString *modelPath;
NSURL *modelURL;
modelPath = [bundle pathForResource:@"MyModel"
                             ofType:@"momd"];
modelURL = [NSURL fileURLWithPath:modelPath];
objModel = [[NSManagedObjectModel alloc]
            initWithContentsOfURL:modelURL];

This allows you to explicitly load the current version of your object model, potentially even in a separate bundle. Rarely will you need to merge multiple object models together, and if you do there are ways around that while still being able to explicitly point Core Data at the latest versions of them all.

The standard Core Data template assumes your NSManagedObjectContext will be passed from view-controller to view-controller manually. This is extremely cumbersome and is prone to error. It’s much easier to provide a centralized mechanism for getting at an object context.

I tend to create a central singleton that implements my Core Data stack, which provides a convenient property for accessing the main object context. This allows me to do something like the following:

1
2
3
4
5
6
NSFetchRequest *request;
// Configure the request
NSManagedObjectContext *context;
context = [DatabaseManager sharedInstance].mainObjectContext;
results = [context executeFetchRequest:request
                                 error:nil];

Without having to pass the object context around explicitly you can gain access to your Core Data model wherever you need to.

Additionally, since NSManagedObjectContext objects aren’t thread-safe (and you should really have one context per running thread) you can create a convenience method for creating new object contexts without needing to reference your specific object model or persistent store every time.

The default location for saving your SQLite database is in the Application Documents directory. This becomes a problem if you want to share documents your application creates with your user or to other applications (using UIFileSharingEnabled). These files should be saved in the /Library directory so they can be backed up, but not shared with the user.

Additionally the default implementation doesn’t enable encryption of your SQLite database file, which is simple enough to do and can be vital for the protection of your database content.  The iPhone 3GS and above has hardware-level encryption and supports varying levels of security. A single call enables extremely robust file protection featuresfor files stored on disk, but only provides extra security if you ask for it.

All developers are inherently lazy and don’t want to do any unnecessary work. This is why we try to reuse as much of our code as possible. We do this by creating common classes that we can share not only between our own projects, but between each other. There are times that sharing individual files becomes a burden though, and you’d like to create some sort of reusable static library that can easily be linked into your project. I go so far as to create a separate static library target in my iOS applications, and anything not directly related to my application is created in this library.

Recently I tried to take this same approach with the Core Data implementation for a project of mine, and this is where I started to run into murky waters. The implementation classes for my various Core Data entities could easily be compiled into a static library, as well as the singleton class I use to manage my Core Data stack (persistent store, managed object model, etc). However this is all meaningless unless you have the Managed Object Model for your Core Data implementation, and accessing that from within a static library isn’t all that intuitive. There are ways to work around this however, though you have to make some tweaks to make it work.

The important thing to know about Core Data is that the Managed Object Model file (with a .mom extension which explains my “Your Mom” joke above) is created at compile-time by Xcode. This however isn’t source code, and can’t be directly stored in a static library. Additionally the default method for accessing your .mom file assumes it lives in your main bundle.

The simplest approach for working around this is to include a resource bundle along with your static library. By dragging your .xcdatamodeld directory into the “Compile Sources” section of your bundle, or set your custom bundle to be the target of your Core Data model when adding it to your project.

Once you have your bundle, you simply change your code that constructs your NSManagedObjectModel object to look in the new bundle instead.

1
2
3
4
5
6
7
8
9
10
11
12
13
NSString *bundlePath, *modelPath;
NSBundle *bundle;
NSURL *bundleURL;
NSManagedObjectModel *objModel;
bundlePath = [[NSBundle mainBundle]
                    pathForResource:@"MyLibrary"
                             ofType:@"bundle"];
bundle = [NSBundle bundleWithPath:bundlePath];
modelPath = [bundle pathForResource:@"MyModel"
                             ofType:@"momd"];
modelURL = [NSURL fileURLWithPath:modelPath];
objModel = [[NSManagedObjectModel alloc]
            initWithContentsOfURL:modelURL];

When working on an update to myDrumPad I encountered many of the problems I describe above, especially since I needed to migrate my user’s databases to a new version of the object model. In addition to the updated database I also wanted to share my audio engine with another music-related app I’m designing, so my goals for this updated included:

  1. Migrate the datamodel to a new version.
  2. Unit test the migration of data from the old to the new version.
  3. Move the datamodel into a static library.
  4. Minimize the amount of boilerplate code that needed to reside in the application delegate class.
  5. Allow these classes to be reused.

While I never got unit testing data migrations working, I did manage to accomplish my other problems.  In fact the Singleton class I created, DataManager, has already been reused in 4 other projects. So to get you started on the right foot, here’s an example of how you can start where Apple’s templates leave off. Simply add this singleton to an Xcode project, optionally create your own static library target and bundle, and tie all the ends together to create your own application.

// DataManager.h
#import
#import

extern NSString * const DataManagerDidSaveNotification;
extern NSString * const DataManagerDidSaveFailedNotification;

@interface DataManager : NSObject {
}

@property (nonatomic, readonly, retain) NSManagedObjectModel *objectModel;
@property (nonatomic, readonly, retain) NSManagedObjectContext *mainObjectContext;
@property (nonatomic, readonly, retain) NSPersistentStoreCoordinator *persistentStoreCoordinator;

+ (DataManager*)sharedInstance;
- (BOOL)save;
- (NSManagedObjectContext*)managedObjectContext;

@end

// DataManager.m
#import "DataManager.h"

NSString * const DataManagerDidSaveNotification = @"DataManagerDidSaveNotification";
NSString * const DataManagerDidSaveFailedNotification = @"DataManagerDidSaveFailedNotification";

@interface DataManager ()

- (NSString*)sharedDocumentsPath;

@end

@implementation DataManager

@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
@synthesize mainObjectContext = _mainObjectContext;
@synthesize objectModel = _objectModel;

NSString * const kDataManagerBundleName = @"MyApp";
NSString * const kDataManagerModelName = @"MyApp";
NSString * const kDataManagerSQLiteName = @"MyApp.sqlite";

+ (DataManager*)sharedInstance {
static dispatch_once_t pred;
static DataManager *sharedInstance = nil;

dispatch_once(&pred, ^{ sharedInstance = [[self alloc] init]; });
return sharedInstance;
}

- (void)dealloc {
[self save];

[_persistentStoreCoordinator release];
[_mainObjectContext release];
[_objectModel release];

[super dealloc];
}

- (NSManagedObjectModel*)objectModel {
if (_objectModel)
return _objectModel;

NSBundle *bundle = [NSBundle mainBundle];
if (kDataManagerBundleName) {
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:kDataManagerBundleName ofType:@"bundle"];
bundle = [NSBundle bundleWithPath:bundlePath];
}
NSString *modelPath = [bundle pathForResource:kDataManagerModelName ofType:@"momd"];
_objectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:[NSURL fileURLWithPath:modelPath]];

return _objectModel;
}

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

// Get the paths to the SQLite file
NSString *storePath = [[self sharedDocumentsPath] stringByAppendingPathComponent:kDataManagerSQLiteName];
NSURL *storeURL = [NSURL fileURLWithPath:storePath];

// Define the Core Data version migration options
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
nil];

// Attempt to load the persistent store
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.objectModel];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:options
error:&error]) {
NSLog(@"Fatal error while creating persistent store: %@", error);
abort();
}

return _persistentStoreCoordinator;
}

- (NSManagedObjectContext*)mainObjectContext {
if (_mainObjectContext)
return _mainObjectContext;

// Create the main context only on the main thread
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(mainObjectContext)
  withObject:nil
waitUntilDone:YES];
return _mainObjectContext;
}

_mainObjectContext = [[NSManagedObjectContext alloc] init];
[_mainObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];

return _mainObjectContext;
}

- (BOOL)save {
if (![self.mainObjectContext hasChanges])
return YES;

NSError *error = nil;
if (![self.mainObjectContext save:&error]) {
NSLog(@"Error while saving: %@\n%@", [error localizedDescription], [error userInfo]);
[[NSNotificationCenter defaultCenter] postNotificationName:DataManagerDidSaveFailedNotification
object:error];
return NO;
}

[[NSNotificationCenter defaultCenter] postNotificationName:DataManagerDidSaveNotification object:nil];
return YES;
}

- (NSString*)sharedDocumentsPath {
static NSString *SharedDocumentsPath = nil;
if (SharedDocumentsPath)
return SharedDocumentsPath;

// Compose a path to the /Database directory
NSString *libraryPath = [[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0] retain];
SharedDocumentsPath = [[libraryPath stringByAppendingPathComponent:@"Database"] retain];

// Ensure the database directory exists
NSFileManager *manager = [NSFileManager defaultManager];
BOOL isDirectory;
if (![manager fileExistsAtPath:SharedDocumentsPath isDirectory:&isDirectory] || !isDirectory) {
NSError *error = nil;
NSDictionary *attr = [NSDictionary dictionaryWithObject:NSFileProtectionComplete
forKey:NSFileProtectionKey];
[manager createDirectoryAtPath:SharedDocumentsPath
withIntermediateDirectories:YES
attributes:attr
error:&error];
if (error)
NSLog(@"Error creating directory path: %@", [error localizedDescription]);
}

return SharedDocumentsPath;
}

- (NSManagedObjectContext*)managedObjectContext {
NSManagedObjectContext *ctx = [[[NSManagedObjectContext alloc] init] autorelease];
[ctx setPersistentStoreCoordinator:self.persistentStoreCoordinator];

return ctx;
}

@end

阅读(924) | 评论(0) | 转发(0) |
0

上一篇:Fink Usage

下一篇:Python Note

给主人留下些什么吧!~~