Skip to content

Instantly share code, notes, and snippets.

@adib
Created September 6, 2012 06:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adib/3652120 to your computer and use it in GitHub Desktop.
Save adib/3652120 to your computer and use it in GitHub Desktop.
Asynchronous saving of Core Data document for Mac OS X
//
// BSManagedDocument.h
// Scuttlebutt
//
// Created by Sasmito Adibowo on 29-08-12.
// Copyright (c) 2012 Basil Salad Software. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@interface BSManagedDocument : NSDocument
+ (NSString *)persistentStoreName;
- (NSString *)persistentStoreTypeForFileType:(NSString *)fileType;
- (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)url ofType:(NSString *)fileType modelConfiguration:(NSString *)configuration storeOptions:(NSDictionary *)storeOptions error:(NSError **)error;
- (BOOL)readAdditionalContentFromURL:(NSURL *)absoluteURL error:(NSError * __autoreleasing*)error;
- (BOOL)writeAdditionalContent:(id)content toURL:(NSURL *)absoluteURL originalContentsURL:(NSURL *)absoluteOriginalContentsURL error:(NSError * __autoreleasing*)error;
- (id)additionalContentForURL:(NSURL *)absoluteURL error:(NSError * __autoreleasing*)error;
- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError * __autoreleasing*)error;
@property(nonatomic, strong, readonly) NSManagedObjectModel *managedObjectModel;
@property(nonatomic, strong, readonly) NSManagedObjectContext *managedObjectContext;
@end
// ---
//
// BSManagedDocument.m
// Scuttlebutt
//
// Created by Sasmito Adibowo on 29-08-12.
// Copyright (c) 2012 Basil Salad Software. All rights reserved.
//
#import "FoundationAdditionsMacros.h"
#import "BSManagedDocument.h"
#import "AppKitAdditions.h"
#if !__has_feature(objc_arc)
#error Need automatic reference counting to compile this.
#endif
@interface BSManagedDocument()
@end
@implementation BSManagedDocument {
NSPersistentStoreCoordinator* _persistentStoreCoordinator;
NSManagedObjectContext* _rootManagedObjectContext;
NSManagedObjectContext* _managedObjectContext;
NSOperationQueue* _backgroundQueue;
NSManagedObjectModel* _managedObjectModel;
}
/*
Returns the URL for the wrapped Core Data store file. This appends the StoreFileName to the document's path.
*/
- (NSURL *)storeURLFromURL:(NSURL *)containerURL {
NSURL* storeURL = [containerURL URLByAppendingPathComponent:[[self class] persistentStoreName]];
return storeURL;
}
#pragma mark UIManagedDocument-inspired methods
+(NSString *)persistentStoreName
{
return @"persistentStore";
}
-(NSManagedObjectModel *)managedObjectModel
{
if (!_managedObjectModel) {
NSBundle* modelBundle = [NSBundle mainBundle];
NSArray* bundleArray = [NSArray arrayWithObject:modelBundle];
_managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:bundleArray];
}
return _managedObjectModel;
}
-(NSString *)persistentStoreTypeForFileType:(NSString *)fileType
{
return NSSQLiteStoreType;
}
- (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)url ofType:(NSString *)fileType modelConfiguration:(NSString *)configuration storeOptions:(NSDictionary *)storeOptions error:(NSError **)error
{
NSManagedObjectModel* model = [self managedObjectModel];
NSPersistentStoreCoordinator* coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
NSError* pscError = nil;
if ([coordinator addPersistentStoreWithType:[self persistentStoreTypeForFileType:fileType] configuration:configuration URL:url options:storeOptions error:&pscError]) {
NSManagedObjectContext* parentContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[parentContext setPersistentStoreCoordinator:coordinator];
@synchronized(self) {
_rootManagedObjectContext = parentContext;
_persistentStoreCoordinator = coordinator;
}
return YES;
} else {
if (error) {
*error = pscError;
}
return NO;
}
return NO;
}
- (id)additionalContentForURL:(NSURL *)absoluteURL error:(NSError * __autoreleasing*)error
{
return nil;
}
- (BOOL)readAdditionalContentFromURL:(NSURL *)absoluteURL error:(NSError * __autoreleasing*)error
{
return YES;
}
- (BOOL)writeAdditionalContent:(id)content toURL:(NSURL *)absoluteURL originalContentsURL:(NSURL *)absoluteOriginalContentsURL error:(NSError * __autoreleasing*)error
{
return NO;
}
#pragma mark NSDocument
- (id)init
{
self = [super init];
if (self) {
// Add your subclass-specific initialization here.
}
return self;
}
- (NSString *)windowNibName
{
// Override returning the nib file name of the document
// If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this method and override -makeWindowControllers instead.
//return @"BSManagedDocument";
return nil;
}
- (void)windowControllerDidLoadNib:(NSWindowController *)aController
{
[super windowControllerDidLoadNib:aController];
// Add any code here that needs to be executed once the windowController has loaded the document's window.
}
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError
{
// Insert code here to write your document to data of the specified type. If outError != NULL, ensure that you create and set an appropriate error when returning nil.
// You can also choose to override -fileWrapperOfType:error:, -writeToURL:ofType:error:, or -writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead.
if (outError) {
*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:NULL];
}
return nil;
}
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError
{
if (outError) {
*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:NULL];
}
return YES;
}
+ (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName
{
DebugLog(@"asked type: %@",typeName);
return YES;
}
+ (BOOL)autosavesInPlace
{
return YES;
}
+ (BOOL)autosavesDrafts
{
return NO;
}
+(BOOL)preservesVersions
{
return NO;
}
- (BOOL)isEntireFileLoaded
{
return NO;
}
- (BOOL)canAsynchronouslyWriteToURL:(NSURL *)url ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation
{
return YES;
}
- (void)saveToURL:(NSURL *)url ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation completionHandler:(void (^)(NSError *errorOrNil))completionHandler
{
// save the main thread context if we have one.
DebugLog(@"saving to URL: %@",url);
if (_managedObjectContext) {
NSError* mainContextError = nil;
if(![_managedObjectContext save:&mainContextError]) {
completionHandler(mainContextError);
return;
}
}
[super saveToURL:url ofType:typeName forSaveOperation:saveOperation completionHandler:completionHandler];
}
- (BOOL)saveToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation error:(NSError * __autoreleasing*)outError
{
if (_managedObjectContext) {
NSError* mainContextError = nil;
if(![_managedObjectContext save:&mainContextError]) {
if (outError) {
*outError = mainContextError;
}
return NO;
}
}
return [super saveToURL:absoluteURL ofType:typeName forSaveOperation:saveOperation error:outError];
}
- (BOOL)writeSafelyToURL:(NSURL *)inAbsoluteURL ofType:(NSString *)inTypeName forSaveOperation:(NSSaveOperationType)inSaveOperation error:(NSError **)outError
{
DebugLog(@"Saving operation %@ to URL: %@",BSStringFromSaveOperationType(inSaveOperation),inAbsoluteURL);
NSError* additionalContentError = nil;
id additionalContent = [self additionalContentForURL:inAbsoluteURL error:&additionalContentError];
if (additionalContentError) {
if (outError) {
*outError = additionalContentError;
}
return NO;
}
//NSString *filePath = [inAbsoluteURL path];
NSURL *originalURL = [self fileURL];
NSDictionary *fileAttributes = [self fileAttributesToWriteToURL:inAbsoluteURL ofType:inTypeName forSaveOperation:inSaveOperation originalContentsURL:originalURL error:outError];
/// ---- Main thread unblocked at this point ---- ///
[self unblockUserInteraction];
NSFileWrapper *filewrapper = nil;
// Depending on the type of save operation:
if (inSaveOperation == NSSaveToOperation) {
// not supported
if (outError) {
NSDictionary* errorInfo = @{
NSLocalizedDescriptionKey : NSLocalizedString(@"Core Data does not support saving changes to a new document while maintaining the unsaved state in the current document.", @"Error")
};
*outError = [NSError errorWithDomain:NSCocoaErrorDomain code:unimpErr userInfo:errorInfo];
}
return NO;
} else if (inSaveOperation == NSSaveAsOperation || inSaveOperation == NSAutosaveAsOperation) {
// Nothing exists at the URL: set up the directory and migrate the Core Data store.
filewrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil];
// Need to write once so there's somewhere for the store file to go.
//[filewrapper writeToFile:filePath atomically:NO updateFilenames:NO];
NSError* fileWrapperFirstWriteError = nil;
if (![filewrapper writeToURL:inAbsoluteURL options:NSFileWrapperWritingWithNameUpdating originalContentsURL:originalURL error:&fileWrapperFirstWriteError]) {
if (outError) {
*outError = fileWrapperFirstWriteError;
}
return NO;
};
// Now, the Core Data store...
NSURL *storeURL = [self storeURLFromURL:inAbsoluteURL];
NSURL *originalStoreURL = [self storeURLFromURL:originalURL];
if (originalStoreURL != nil) {
// This is a "Save As", so migrate the store to the new URL.
NSPersistentStoreCoordinator *coordinator = nil;
@synchronized(self) {
coordinator = _persistentStoreCoordinator;
}
id originalStore = [coordinator persistentStoreForURL:originalStoreURL];
NSPersistentStore* migratedStore = [coordinator migratePersistentStore:originalStore toURL:storeURL options:nil withType:[self persistentStoreTypeForFileType:inTypeName] error:outError];
if (!migratedStore) {
return NO;
}
} else {
// just configure the store
if(![self configurePersistentStoreCoordinatorForURL:storeURL ofType:[self persistentStoreTypeForFileType:inTypeName] modelConfiguration:nil storeOptions:nil error:outError]) {
return NO;
}
}
} else { // This is not a Save-As operation.
// Just create a file wrapper pointing to the existing URL.
NSError* fileWrapperError = nil;
filewrapper = [[NSFileWrapper alloc] initWithURL:inAbsoluteURL options:0 error:&fileWrapperError];
DebugLog(@"fileWrapperError: %@",fileWrapperError);
}
NSManagedObjectContext* rootContext = nil;
@synchronized(self) {
rootContext = _rootManagedObjectContext;
}
NSError __block* rootError = nil;
[rootContext performBlockAndWait:^{
NSError* parentError = nil;
[rootContext save:&parentError];
if (parentError) {
ErrorLog(@"Could not save root context: %@", parentError);
rootError = parentError;
}
}];
if (rootError) {
if (outError) {
*outError = rootError;
}
return NO;
}
// save non core-data portion.
if (additionalContent) {
NSError* additionalContentWriteError = nil;
if(![self writeAdditionalContent:additionalContent toURL:inAbsoluteURL originalContentsURL:originalURL error:&additionalContentWriteError]) {
if (additionalContentWriteError) {
ErrorLog(@"Could not save additioanl content: %@", additionalContentWriteError);
if (outError) {
*outError = additionalContentWriteError;
}
}
return NO;
}
}
NSFileManager* const fileManager = [NSFileManager defaultManager];
// Set the appropriate file attributes (such as "Hide File Extension")
if (fileAttributes) {
if ([inAbsoluteURL isFileURL]) {
NSString* path = [inAbsoluteURL path];
NSError* fileError = nil;
[fileManager setAttributes:fileAttributes ofItemAtPath:path error:&fileError];
if (fileError) {
ErrorLog(@"Error '%@' updating attributes of file '%@' to %@",fileError,path,fileAttributes);
}
}
}
return YES;
}
- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError * __autoreleasing*)error {
BOOL success = NO;
// Create a file wrapper for the document package.
NSFileWrapper *directoryFileWrapper = [[NSFileWrapper alloc] initWithPath:[absoluteURL path]];
// File wrapper for the Core Data store within the document package.
NSFileWrapper *dataStore = [[directoryFileWrapper fileWrappers] objectForKey:[[self class] persistentStoreName]];
if (dataStore != nil) {
NSString *path = [[absoluteURL path] stringByAppendingPathComponent:[dataStore filename]];
NSURL *storeURL = [NSURL fileURLWithPath:path];
// Set the document persistent store coordinator to use the internal Core Data store.
success = [self configurePersistentStoreCoordinatorForURL:storeURL ofType:typeName
modelConfiguration:nil storeOptions:nil error:error];
}
if (!success) {
return NO;
}
// Don't read anything else if reading the main store failed.
if (![self readAdditionalContentFromURL:absoluteURL error:error]) {
return NO;
}
return YES;
}
-(NSUndoManager *)undoManager
{
return [self.managedObjectContext undoManager];
}
-(void)setUndoManager:(NSUndoManager *)undoManager
{
[self.managedObjectContext setUndoManager:undoManager];
}
#pragma mark Property Access
-(NSManagedObjectContext *)managedObjectContext
{
if (!_managedObjectContext) {
@synchronized(self) {
if (_rootManagedObjectContext) {
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_rootManagedObjectContext];
}
}
}
return _managedObjectContext;
}
@end
@jerrykrinock
Copy link

Hello, adib.

Have you written a blog post or article that explains how this works? If so, please link me to it. I found this code directly in a Google search.

Otherwise, I have a question. In Apple's NSPersistentDocument Class Reference [1], at the end of the Overview, they state: "NSPersistentDocument does not support NSDocument’s asynchronous saving API as that API requires accessing the document’s state on multiple threads and that violates NSManagedObjectContext’s requirements." Regarding your code here, I interpret that to mean "You can't do this." I mean, -[NSPersistentDocument canAsynchronouslyWriteToURL:ofType:forSaveOperation:] must return NO.

So, I'm wondering how well your code here works. Have you some trick which works around the limitation stated by Apple? Let me give you my answer. I tried asynchronous saving back when it was first released in 10.7. It worked > 95% of the time, but in a few hard-to-reproduce corner cases I would get one of those dreaded autosave hangs. After several weeks of working with Apple on it, they informed me that the answer was it wouldn't work and don't do it. I filed a bug on the documentation which is likely responsible for them having added the prohibition which I quoted above. Of course, things may have improved in 10.8 and 10.9, but that prohibition is still officially in the OS X 10.9 Doc Set.

Thanks!

Jerry Krinock

[1] https://developer.apple.com/library/mac/documentation/cocoa/reference/ApplicationKit/Classes/NSPersistentDocument_Class/Reference/Reference.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment