I'm guessing you're developing for OS X / macOS (NSTreeController
& NSOutlineView
). I've no experience with macOS - I develop for iOS - so you might need to take that into account when you're reading my response.
I've not yet made the switch to swift - my code is, perhaps obviously, Objective-C...
I'll start with how I prepare the Core Data stack.
I set up two public properties in the header file:
@property (nonatomic, strong) NSManagedObjectContext *mocPrivate;
@property (nonatomic, strong) NSManagedObjectContext *mocMain;
Although this is unnecessary, I also prefer to set up private properties for my Core Data objects, including, for example:
@property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator;
Once I've pointed to my model URL, established my managed object model NSManagedObjectModel
, pointed to my store URL for my NSPersistentStore
and established my persistent store coordinator NSPersistentStoreCoordinator
(PSC), I set up my two managed object contexts (MOC).
Within the method to "build" my Core Data stack, after I've completed the code per the above paragraph, I then include the following...
if (!self.mocPrivate) {
self.mocPrivate = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[self.mocPrivate setPersistentStoreCoordinator:self.persistentStoreCoordinator];
} else {
// report to console the use of existing MOC
}
if (!self.mocMain) {
self.mocMain = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[self.mocMain setParentContext:self.mocPrivate];
} else {
// report to console the use of existing MOC
}
(I usually include a few NSLog
lines in this code to report to my console but I've excluded that here to keep the code clean.)
Note two important aspects to this code...
- set the private queue MOC to interact with the PSC; and
- set the main queue MOC as the child of the private queue MOC.
Why is this done? First let's highlight a couple of important points:
- Saves to memory are relatively fast; and
- Saves to disc are relatively slow.
The private queue is asynchronous to the main queue. The User Interface (UI) operates on the main queue. The private queue operates on a separate thread "in the background" working to maintain context and coordinate data persistence with the PSC, perfectly managed by Core Data and iOS. The main queue operates on the main thread with the UI.
Written a different way...
- Heavy work completing irregular (managed by Core Data) data persistence to the PSC (saves to disc) is completed in the private queue; and
- Light work completing regular (managed by developer) data persistence to the MOC (saves to memory) is completed in the main queue.
In theory this should ensure your UI is never blocked.
But there is more to this solution. How we manage the "save" process is important...
I write a public method:
- (void)saveContextAndWait:(BOOL)wait;
I call this method from any class that needs to persist data. The code for this public method:
- (void)saveContextAndWait:(BOOL)wait {
// 1. First
if ([self.mocMain hasChanges]) {
// 2. Second
[self.mocMain performBlockAndWait:^{
NSError __autoreleasing *error;
BOOL success;
if (!(success = [self.mocMain save:&error])) {
// error handling
} else {
// report success to the console
}
}];
} else {
NSLog(@"%@ - %@ - CORE DATA - reports no changes to managedObjectContext MAIN_", NSStringFromClass(self.class), NSStringFromSelector(_cmd));
}
// 3. Third
void (^savePrivate) (void) = ^{
NSError __autoreleasing *error;
BOOL success;
if (!(success = [self.mocPrivate save:&error])) {
// error handling
} else {
// report success to the console
}
};
// 4. Fourth
if ([self.mocPrivate hasChanges]) {
// 5. Fifth
if (wait) {
[self.mocPrivate performBlockAndWait:savePrivate];
} else {
[self.mocPrivate performBlock:savePrivate];
}
} else {
NSLog(@"%@ - %@ - CORE DATA - reports no changes to managedObjectContext PRIVATE_", NSStringFromClass(self.class), NSStringFromSelector(_cmd));
}
}
So I'll work through this to explain what is happening.
I offer the developer the option to save and wait (block), and depending on the developer's use of the method saveContextAndWait:wait
, the private queue MOC "saves" using either:
- the
performBlockAndWait
method (developer calls method with wait
= TRUE
or YES
); or
- the
performBlock
method (developer calls method with wait
= FALSE
or NO
).
First, the method checks whether there are any changes to the main queue MOC. Let's not do any work unless we have to!
Second, the method completes a (synchronous) call to performBlockAndWait
on the main queue MOC. This performs the call to save
method in a code block and waits for completion before allowing the code to continue. Remember this is for regular "saves" of small data sets. The (asynchronous) option to call performBlock
is not required here and in fact will derail the effectiveness of the method, as I experienced when I was learning to implement this in my code (failure to persist data due to the save
call on the main queue MOC attempting to complete after completion of the save
on the private queue MOC).
Third, we write a little block within a block that contains the code to save the private queue MOC.
Fourth, the method checks whether there are any changes to the private queue MOC. This may be unnecessary but it is harmless to include here.
Fifth, depending on the option the developer chooses to implement (wait
= YES
or NO
) the method calls either performBlockAndWait
or performBlock
on the block within a block (under third above).
In this last step, regardless of the implementation (wait
= YES
or NO
), the function of persisting data to disc, from the private queue MOC to the PSC, is abstracted to the private queue on an asynchronous thread to the main thread. In theory the "save to disc" via the PSC can take as long as it likes because it has nothing to do with the main thread. AND because the private queue MOC has all the data in memory, the main queue MOC is fully and automatically informed of the changes because it is the child of the private queue MOC.
If you import large volumes of data into app, something I am currently working on implementing, then it makes sense to import this data into the private queue MOC.
The private queue MOC does two things here:
- It coordinates data persistence (to disc) with the PSC;
- Because it is the parent of the main queue MOC (in memory), the main queue MOC will be notified of the data changes in the private queue MOC and merges are managed by Core Data;
Finally, I use NSFetchedResultsController
(FRC) to manage my data fetches, which are all completed against the main queue MOC. This maintains data hierarchy. As changes are made to the data sets in either context, the FRC updates the view.
This solution is simple! (Once I spent weeks wrangling my head around it and another few weeks refining my code.)
There is no requirement to monitor notifications for merges or other changes to MOC. Core Data and iOS handle everything in the background.
So if this doesn't work for you - let me know - I may have excluded or overlooked something as I wrote this code well over a year ago.