Bringing Asynchronous Core Data documents to OS X

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.

Avoid App Review rules by distributing outside the Mac App Store!

Get my FREE cheat sheets to help you distribute real macOS applications directly to power users.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

10 thoughts on “Bringing Asynchronous Core Data documents to OS X

  1. 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.

      1. 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?

        1. 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.

  2. 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.

  3. 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.

      1. 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

  4. 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?