I'm working on an OSX (Mac OS) document based app with Core Data.
Jul 19, 2007 This post is a follow-up to another post I wrote on the very same subject. I am showing here the full implementation of a NSPersistentDocument based class that allows to use package documents embedding a Core Data store. I short, what this post adds to the previous one is: improved encapsulation; NSDocumentController subclass to correctly. The Core Data framework provides a lot of new functionality to Cocoa developers, but manages to do so without creating an immense class hierarchy. NSPersistentDocument: subclass of NSDocument which works with Core Data: Managed Object. Take a look at the Build a Core Data Application tutorial if you'd like to put this into action.
Core Data is a powerful data-management framework introduced with Mac OS X v10.4 Tiger. When you take advantage of Core Data, it slashes the amount of custom code you need to write in order to manage application data. Opening, saving and undo management can all be handled by Core Data, freeing you to concentrate on creating unique features for your application. This video tutorial, which. Core Data is a core competency for Apple developers—but it's notoriously complex, confusing, and crash prone. Luckily, much of this is resolved with iOS 10's new Core Data APIs. Everything has improved: syntax, architecture, performance, and support to recall and undo several layers of changes. Even the whole data container has been simplified. 'If you have a large data set or require a managed object model, you may want to use NSPersistentDocument to create a document-based app that uses the Core Data framework.' - Document-Based App Programming Guide for Mac 'Applications using very large data sets would likely benefit from a custom persistance implementation.
I'm having problems importing in a PrivateContext. Importing in the MainContext works fine, but of course freezes the UI.
The app has a NSPersistentDocument (Document.h and .m).
In this document I have autosavesInPlace enabled (YES).
The Window belonging to the Document is added in makeWindowControllers
- (void)makeWindowControllers {
familyWindowController *myWindow = [[NSStoryboard storyboardWithName:@'Main' bundle:nil] instantiateControllerWithIdentifier:@'Document Window Controller'];
[self addWindowController: myWindow];
myWindow.managedObjectContext = self.managedObjectContext;
}
The WindowController gets his NSManagedObjectContext (MOC) from self.managedObjectContext.
This Window and MOC are given to childViewControllers, like the sheet I use for the import progress from a file.
The import sheet (ViewController) has a NSWindow property and a MOC property which is set by the prepareForSegue.
Next to the MOC from the Main window I have a PrivateContext (MOC).
It turn out that the originally created MOC is NOT of the NSMainQueueConcurrencyType. This is required is you want to use it with a parentContext.
With this code I change it (maybe this is wrong?).
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
context.persistentStoreCoordinator = self.managedObjectContext.persistentStoreCoordinator;
[context setMergePolicy: NSMergeByPropertyStoreTrumpMergePolicy];
_managedObjectContext = context; // _managedObjectContext is the local MOC from the WindowController
The privateContext is created with this code.
_privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_privateContext.parentContext = _managedObjectContext;
_privateContext.undoManager = nil;
I parse an XML with Google’s GDataXMLNode.
Core data objects are created with
What works:
If I don’t change the MOC to a NSMainQueueConcurrencyType, everything works fine, but I have a frozen UI.
What don’t work:
- When I make the MOC into a NSMainQueueConcurrencyType, I can import the data, but it doesn’t save to the file. I suspect something is wrong with the persistentStore, but I don’t see what.
- When I use the PrivateContext (thru the Parent) I can get progress in the UI en UI updates, as long *** I don’t move the Window or trigger a Window refresh. I think this is because of the NSTableViews in the Window, which all use Core Data and binding. So when they refresh the view you get Core Data errors (out of bound…). No Idea how to fix this.
Any ideas are welcome.
An example with OSX NSDocument and Core Data would be even better
The privateContext is created with this code.
_privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_privateContext.parentContext = _managedObjectContext;
_privateContext.undoManager = nil;
I parse an XML with Google’s GDataXMLNode.
Core data objects are created with
What works:
If I don’t change the MOC to a NSMainQueueConcurrencyType, everything works fine, but I have a frozen UI.
What don’t work:
- When I make the MOC into a NSMainQueueConcurrencyType, I can import the data, but it doesn’t save to the file. I suspect something is wrong with the persistentStore, but I don’t see what.
- When I use the PrivateContext (thru the Parent) I can get progress in the UI en UI updates, as long *** I don’t move the Window or trigger a Window refresh. I think this is because of the NSTableViews in the Window, which all use Core Data and binding. So when they refresh the view you get Core Data errors (out of bound…). No Idea how to fix this.
Any ideas are welcome.
An example with OSX NSDocument and Core Data would be even better
Core Data Persistent Packages revisited
19 July 2007
This post is a follow-up to another post I wrote on the very same subject. I am showing here the full implementation of a NSPersistentDocument based class that allows to use package documents embedding a Core Data store.
I short, what this post adds to the previous one is:
- improved encapsulation;
- NSDocumentController subclass to correctly handle the Recent Document menus;
- fixed a problem with NSError handling, though still not doing any proper error management.
Those improvements originated from a discussion with Tim Perrett in the cocoa-dev mailing list and from a comment by Laurent Sansonetti to my original post. Thanks to both.
PersistentPackageDocument Class
The PersistentPackageDocument class can be used as a base class for your document classes whenever you want them use a document package embedding the actual Core Data data store. PersistentPackageDocument derives from NSPersistentDocument and overrides four methods: initWithContentsOfURL_ofType_error, writeToURL_ofType_forSaveOperation_originalContentsURL_error, readFromURL_ofType_error, displayname
. Here’s the code:
class PersistentPackageDocument < OSX::NSPersistentDocument
#-- returns the document name to display in the window title
def displayName
if (fileURL)
documentNameFromDataStoreURL(fileURL)
else
'Untitled'
end
end
Nspersistentdocument Core Data Tutorial For Macbook
#-- returns the package document path by stripping the dataStoreName component
#-- from the data store URL; used in displayName
def documentNameFromDataStoreURL(url)
/([^/]+)/?$/ =~ url.relativePath.gsub(/#{dataStoreName}$/, ')
$1 + ' - View'
end
def dataStoreURLFromPackageURL(url)
dataStorePath = url.relativePath.stringByAppendingPathComponent(dataStoreName)
OSX::NSURL.fileURLWithPath(dataStorePath)
end
def readFromURL_ofType_error(url, type, errorPtr)
path=url.relativePath
if (!OSX::NSFileManager.defaultManager.fileExistsAtPath_isDirectory(path, nil))
#-- YOUR ERROR MANAGEMENT HERE
end
result = super_readFromURL_ofType_error(url, type, nil)
if (!result)
#-- SET ERROR INFORMATION TO BE RETURNED VIA errorPtr.assign(nserror_object)
end
result
end
def writeToURL_ofType_forSaveOperation_originalContentsURL_error(url, type, op, content, errorPtr)
#-- if content is not nil, then we are saving a newly created document
#-- in this case, initWithURL is not called, so we had no chance to fix the url,
#-- let's do it here.
if (content nil)
path = url.relativePath
url = dataStoreURLFromPackageURL(url)
isDirectory = false
if (!OSX::NSFileManager.defaultManager.createDirectoryAtPath_attributes(path, nil))
#-- YOUR ERROR MANAGEMENT HERE, set errorPtr
return false
end
end
ok = super_writeToURL_ofType_forSaveOperation_originalContentsURL_error(url, type, op, content, nil)
Nspersistentdocument Core Data Tutorial For Mac Desktop
if (!ok)
#-- SET ERROR INFORMATION TO BE RETURNED VIA errorPtr.assign(nserror_object)
end
ok
end
def initWithContentsOfURL_ofType_error(url, type, errPtr)
url = dataStoreURLFromPackageURL(url)
ok, err = super_initWithContentsOfURL_ofType_error(url, type, nil)
if (!ok)
#-- SET ERROR INFORMATION TO BE RETURNED VIA errorPtr.assign(nserror_object)
end
ok
end
end
For a more detailed discussion of the rationale behind this implementation, see my previous post.
You should then change your MyDocument class (the one produced by XCode templates) so that it derives from PersistentPackageDocument
instead of NSPersistentDocument
and it adds a dataStoreName
method that returns the data store file name for that specific document. Here an example:
class MyDocument < PersistentPackageDocument
def dataStoreName
'data.xml'
end
#-- default RubyCocoa implementation: managedObjectModel, setManagedObjectContext, windowNibName, etc.
end
Supporting Recent Documents
The PersistentPackageDocumentClass
as given above is fully capable of dealing with package documents embedding a Core Data data store. Unfortunately, it alone cannot ensure that the Recent Documents menu is correctly handled in your application. To that aim, you need to override your NSDocumentController
noteNewRecentDocumentURL
method so that it does some juggling with the path that is stored with the recent document menus.
If your package document is enough rich, chances are that you are already subclassing NSDocumentController
, so overriding noteNewRecentDocumentURL
is a snap. Otherwise, here is a sample subclass:
class PersistentPackageDocumentController < OSX::NSDocumentController
def init
super_init
end
def packageURLFromDataStoreURL(url)
dataStoreName = currentDocument.dataStoreName
OSX::NSURL.fileURLWithPath(url.relativePath.gsub(/#{dataStoreName}$/, '))
end
Nspersistentdocument Core Data Tutorial For Macbook Pro
def noteNewRecentDocumentURL(url)
if (currentDocument)
super_noteNewRecentDocumentURL(packageURLFromDataStoreURL(url))
end
end
end
As already mentioned, the key point is the method noteNewRecentDocumentURL
, while packageURLFromDataStoreURL
is just responsible for string manipulation. Note also that packageURLFromDataStoreURL
accesses the current document to retrieve its dataStoreName
and this forces to guard against the case when there is no current document. There are many alternative implementation of this behaviour, in particular you could define the method dataStoreName
in the document controller class and let PersistentPackageDocument access it there. This approach has the adavantage that a “current” document controller is always there, but for presentation reasons it is not taken here.
Sublassing NSDocumentController
has its own particularities. The easiest way to do it is in Interface Builder MainMenu.nib file. Just subclass and instantiate it in the nib and the above code will be used for your document shared controller. Read this FAQ for more information.
Nspersistentdocument Core Data Tutorial For Mac Shortcut
Summing up
The two classes defined above will allow you to easily integrate package documents in your Core Data application.
Nspersistentdocument Core Data Tutorial For Mac Windows 10
One final note: make sure you define your document classes as packages in your target properties.