When Apple released iOS 5 and OS X Lion, they also brought a major improvement to Core Data: official multithreading support. You can now associate NSManagedObjectContext
objects with specific threads or let them have their own operation queues for executing core data operations. Along with this improvement, Apple also brought UIManagedDocument
that performs I/O operations in the background and doesn’t block the user interface thread.
However, OS X is not as fortunate. Although NSDocument
now supports background I/O operations, its core data subclass, NSPersistentDocument
does not perform I/O in the background and it has no support for anything like iOS’ UIManagedDocument
. Even in its subsequent iteration in Mountain Lion, Apple have not brought the capability of UIManagedDocument
into AppKit.
Thankfully it isn’t too hard to come up with our own document class backed by Core Data and with background I/O. We just need to manage the Core Data stack ourselves and make it play well with the rest of the document infrastructure. I’ll show you how to create this kind of NSDocument
subclass in this post.
The first step is configuring the core data stack, which is the persistent store coordinator and the managed object context objects and how they see each other. Similar to UIDocument
, we use a pair of NSManagedObjectContext
instances coupled in a parent-child relationship. The parent context is used only for saving in the background and will remain private to the document. Whereas the child context is the one that we expose outside and will be used by most user interface objects in the main thread.
NSManagedObjectContext* parentContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; [parentContext setPersistentStoreCoordinator: ...]; NSManagedObjectContext* mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; [mainContext setParentContext:parentContext];
The second step is handling document saves. Each NSManagedObjectContext
in the pair needs to be saved in order, the main context before the parent in their appropriate threads. To save the main context, you’ll need to override these methods and save it before you pass the call to super
:
saveToURL: ofType: forSaveOperation: completionHandler:
saveToURL: ofType: forSaveOperation: error:
Then the parent context is saved in writeSafelyToURL: ofType: forSaveOperation: error:
where it’s called in a background queue. In that method, you can run some code that accesses UI data in the first half of this method — NSDocument blocks the main thread before calling this method. You might need to copy any mutable objects in this first half so it doesn’t get mutated by the main thread when you unblock it. When you’re done you call unblockUserInteraction
to get the main thread unblocked and then you can continue with file I/O and saving the parent thread.
- (BOOL)writeSafelyToURL:(NSURL *)inAbsoluteURL ofType:(NSString *)inTypeName forSaveOperation:(NSSaveOperationType)inSaveOperation error:(NSError **)outError { // access data here [self unblockUserInteraction]; // unblock main thread // do I/O here }
Finally in the last step is synchronizing NSDocument
‘s file modification date with the file storage’s modification date. Apparently with SQLite persistent store type, when a child context saves it also writes something to the persistent store & nudges the file’s modification date. Thus we need to listen to context save events and tell NSDocument that the underlying file have changed. If we don’t do this, we’ll get a “file modified by another application” error message whenever we try to save the document.
-(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]; } } }
So that’s just about all there is to write multithreaded document-based Core Data apps on OS X. You can find the complete source code to the class in the gist below, under the liberal BSD license. Another plus point for using my implementation is that you’ll get support for file packages for free.
You say:
“Apparently with SQLite persistent store type, when a child context saves it also writes something to the persistent store & nudges the file’s modification date”
How did you determine this? I’ve not tested with the SQLite store yet, but can confirm that the binary store definitely does NOT exhibit such behaviour.
I discovered this during development of my own app, hence this class. Likely when a new managed object wants a permanent ID and that triggers a save along the chain.
So your code is requesting a permanent ID for object(s)? If so, that is explicitly document as part of the docs for -obtainPermanentIDsForObjects:…
I’d argue that your saving code shouldn’t be dealing with that; it’s up to the caller instead.
Or did you find that the main context was somehow obtaining permanent IDs as part of saving?
That’s what I believe the case – I haven’t done any extensive testing in this area. Anyway some of the code that wants permanent IDs are part of the managed object class’ themselves (roughly following the Active Record pattern) and I don’t want to pollute the managed object code too much with OSX-specific stuff (in this context, NSDocument), in case I want to reuse the code for iOS. Whereas some others are primarily data-crunching code that I also don’t want to be dependent on NSDocument.
Without updating NSDocument’s notion of the file’s date when the main context saves, I get “document already modified by other application” errors. Hence the workaround. Granted this was tested with the SQLite store, not with others.
You say:
“Apparently with SQLite persistent store type, when a child context saves it also writes something to the persistent store & nudges the file’s modification date”
How did you determine this? I’ve not tested with the SQLite store yet, but can confirm that the binary store definitely does NOT exhibit such behaviour.
Maybe it’s just me but there seems to be a huge flaw in the Autosave Elsewhere operation? This is functionality required when “Duplicating” a document which is standard in the file menu.
Really? I don’t use Autosave Elsewhere in the app I’m developing right now (Scuttlebutt), but you’re welcome to fork the gist and add in a working implementation.
Thanks
—
Sasmito Adibowo
http://cutecoder.org
Autosave Elsewhere I believe is introduced in 10.8 though I could be wrong, it happens if you duplicate a document before it has been saved or autosaved before. For core data this means you wont have a persistent store yet.
Also in your example code you’re missing File Coordination that is new in 10.7 and also atomicity of the writes. IE: Your file wrapper may save but what if core data doesn’t?
Reality is it’s nearly impossible to completely address atomicity when Core Data is used inside a File Wrapper due to the complexities and trade-offs. What sucks most is with SQLite store types you lose incremental saving if you want true atomicity
You observe NSManagedObjectContextDidSaveNotification in the main thread MOC in order to update the file modification date. The main thread MOC saves into the root MOC, not directly to the file system, so if the root MOC, which runs in a background thread, takes a longer time to save, you might long be done updating the modification date, and so the actual file modification takes place later, which should cause the “file was saved by another application” error. Shouldn’t you rather observe the DidSave of the root MOC instead and do
setFileModificationDate there?
This is in addition to NSDocument’s background queue which also sets the modification date upon return of the writeXXX method.