Skip to content

Instantly share code, notes, and snippets.

@adib
Created September 11, 2012 06:07
Show Gist options
  • Save adib/3696343 to your computer and use it in GitHub Desktop.
Save adib/3696343 to your computer and use it in GitHub Desktop.
An NSDocument subclass that supports asynchronous Core Data operations
//
// BSManagedDocument.h
//
// Created by Sasmito Adibowo on 29-08-12.
// Copyright (c) 2012 Basil Salad Software. All rights reserved.
// http://basilsalad.com
//
// Licensed under the BSD License <http://www.opensource.org/licenses/bsd-license>
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
#import <Cocoa/Cocoa.h>
/**
A document class that supports Core Data background operations.
Just like UIManagedDocument but for OS X.
*/
@interface BSManagedDocument : NSDocument
+ (NSString *)persistentStoreName;
- (NSString *)persistentStoreTypeForFileType:(NSString *)fileType;
- (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)url ofType:(NSString *)fileType modelConfiguration:(NSString *)configuration storeOptions:(NSDictionary *)storeOptions error:(NSError * __autoreleasing*)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;
/**
No-op, like NSPersistentDocument
*/
-(void)setUndoManager:(NSUndoManager *)undoManager;
/**
No-op, like NSPersistentDocument
*/
-(void)setHasUndoManager:(BOOL)hasUndoManager;
-(BOOL)isDocumentEdited;
-(void) managedObjectContextDidSave:(NSNotification*) notification;
@property(strong,readonly) NSManagedObjectModel *managedObjectModel;
@property(strong,readonly) NSManagedObjectContext *managedObjectContext;
@end
// ---
//
// BSManagedDocument.m
//
// Created by Sasmito Adibowo on 29-08-12.
// Copyright (c) 2012 Basil Salad Software. All rights reserved.
// http://basilsalad.com
//
// Licensed under the BSD License <http://www.opensource.org/licenses/bsd-license>
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
#if !__has_feature(objc_arc)
#error Need automatic reference counting to compile this.
#endif
#import <objc/message.h>
#import "FoundationAdditionsMacros.h"
#import "BSManagedDocument.h"
#import "AppKitAdditions.h"
@interface BSManagedDocument()
@property (strong,readonly) NSManagedObjectContext* rootManagedObjectContext;
@end
@implementation BSManagedDocument {
NSPersistentStoreCoordinator* _persistentStoreCoordinator;
NSPersistentStore* _persistentStore;
NSManagedObjectContext* _rootManagedObjectContext;
NSManagedObjectContext* _managedObjectContext;
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 persistentStoreName]];
return storeURL;
}
-(void)managedObjectContextDidSave:(NSNotification *)notification
{
id sender = notification.object;
if (sender == _managedObjectContext) {
NSFileManager* fileManager = [NSFileManager defaultManager];
NSURL* fileURL = [self fileURL];
NSDictionary* fileAttributes = [fileManager attributesOfItemAtPath:[fileURL path] error:nil];
NSDate* modificationDate = fileAttributes[NSFileModificationDate];
if (modificationDate) {
// set the modification date to prevent NSDocument's "file was saved by another application" error.
[self setFileModificationDate:modificationDate];
}
}
}
#pragma mark UIManagedDocument-inspired methods
+(NSString *)persistentStoreName
{
return @"persistentStore";
}
-(NSManagedObjectModel *)managedObjectModel
{
@synchronized(self) {
if (!_managedObjectModel) {
NSBundle* modelBundle = [NSBundle mainBundle];
_managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:@[modelBundle]];
}
return _managedObjectModel;
}
}
-(NSString *)persistentStoreTypeForFileType:(NSString *)fileType
{
return NSSQLiteStoreType;
}
- (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)url ofType:(NSString *)fileType modelConfiguration:(NSString *)configuration storeOptions:(NSDictionary *)storeOptions error:(NSError * __autoreleasing*)error
{
NSManagedObjectModel* model = [self managedObjectModel];
NSPersistentStoreCoordinator* coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
NSError* pscError = nil;
NSPersistentStore* persistentStore = [coordinator addPersistentStoreWithType:[self persistentStoreTypeForFileType:fileType] configuration:configuration URL:url options:storeOptions error:&pscError];
if (persistentStore) {
@synchronized(self) {
_persistentStoreCoordinator = coordinator;
_persistentStore = persistentStore;
NSManagedObjectContext* rootContext = self.rootManagedObjectContext;
[rootContext performBlock:^{
[rootContext setPersistentStoreCoordinator: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 YES;
}
#pragma mark NSDocument
+ (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)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
{
if (saveOperation == NSSaveToOperation) {
return NO;
}
return YES;
}
-(void)autosaveDocumentWithDelegate:(id)delegate didAutosaveSelector:(SEL)didAutosaveSelector contextInfo:(void *)contextInfo
{
DebugLog(@"Autosaving with delegate: %@",delegate);
if (_managedObjectContext) {
NSError* mainContextError = nil;
if(![_managedObjectContext save:&mainContextError]) {
ErrorLog(@"Failed to autosave: %@",mainContextError);
objc_msgSend(delegate, didAutosaveSelector,self,NO,contextInfo);
return;
}
}
[super autosaveDocumentWithDelegate:delegate didAutosaveSelector:didAutosaveSelector contextInfo:contextInfo];
}
- (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.", @"Managed Document Error"),
NSURLErrorKey : inAbsoluteURL
};
*outError = [NSError errorWithDomain:NSOSStatusErrorDomain 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.
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 class] _storeURLFromURL:inAbsoluteURL];
NSURL *originalStoreURL = [[self class] _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];
if (fileWrapperError) {
if (outError) {
*outError = fileWrapperError;
}
return NO;
}
}
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*)outError {
BOOL success = NO;
// Create a file wrapper for the document package.
NSError* fileWrapperError = nil;
NSFileWrapper* directoryFileWrapper = [[NSFileWrapper alloc] initWithURL:absoluteURL options: NSFileWrapperReadingImmediate error:&fileWrapperError];
if (fileWrapperError) {
ErrorLog(@"fileWrapperError: %@",fileWrapperError);
if (outError) {
*outError = fileWrapperError;
return NO;
}
}
// File wrapper for the Core Data store within the document package.
NSFileWrapper *dataStore = [[directoryFileWrapper fileWrappers] objectForKey:[[self class] persistentStoreName]];
if (dataStore != nil) {
NSURL* storeURL = [absoluteURL URLByAppendingPathComponent:[dataStore filename]];
// Set the document persistent store coordinator to use the internal Core Data store.
success = [self configurePersistentStoreCoordinatorForURL:storeURL ofType:typeName
modelConfiguration:nil storeOptions:nil error:outError];
}
if (!success) {
return NO;
}
// Don't read anything else if reading the main store failed.
if (![self readAdditionalContentFromURL:absoluteURL error:outError]) {
return NO;
}
return YES;
}
-(NSUndoManager *)undoManager
{
return [self.managedObjectContext undoManager];
}
-(void)setUndoManager:(NSUndoManager *)undoManager
{
// no-op, just like NSPersistentDocument
}
-(void)setHasUndoManager:(BOOL)hasUndoManager
{
// no-op
}
-(BOOL)hasUndoManager
{
return YES;
}
-(BOOL)isDocumentEdited
{
return [self.managedObjectContext hasChanges];
}
#pragma mark NSObject
- (id)init
{
self = [super init];
if (self) {
// Add your subclass-specific initialization here.
}
return self;
}
-(void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark Property Access
-(NSManagedObjectContext *)rootManagedObjectContext
{
@synchronized(self) {
if (!_rootManagedObjectContext) {
_rootManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
}
return _rootManagedObjectContext;
}
}
-(NSManagedObjectContext *)managedObjectContext
{
@synchronized(self) {
if (!_managedObjectContext) {
NSManagedObjectContext* parentContext = self.rootManagedObjectContext;
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext performBlock:^{
[_managedObjectContext setParentContext:parentContext];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(managedObjectContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:_managedObjectContext];
}];
}
return _managedObjectContext;
}
}
@end
@adib
Copy link
Author

adib commented Oct 23, 2012

@mikeabdullah
Copy link

Hi, overhauled this code based on our internal class over at https://github.com/karelia/BSManagedDocument/tree/ksmanageddocument

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