MagicalRecord saving takes time - objective-c

i am using magicalrecord in my app to manage coredata, every thing is perfect now but i am having an issue,i created 10k entities using this code:
[MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext * _Nonnull localContext) {
Person *person = [Person MR_createEntityInContext:localContext];
person.organization = [Organization MR_findfirstInContext:localContext];
person.name = name;
person.id = id;
person.age = age;
]};
after creating 10k records when i perform any change in database using magicalrecord it gets stuck for 4 or 5 seconds while saving to root context, which makes UI freez no matter if its on main thread or background thread.
Edited:
i have found the problem if i remove relation (which is organization) from save block it dont get stuck, if i fetch organization from out side the block and assign in inside the save block it throw an exception that organization is created in different context.

//case A
[MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) {
NSLog(#"current thread is main thread %d and thread is %#", [NSThread isMainThread], [NSThread currentThread]);
//some ManagedObject code
...
}];
//case B
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
NSLog(#"current thread is main thread %d and thread is %#", [NSThread isMainThread], [NSThread currentThread]);
//some ManagedObject code
...
}];
test this code, you will find case A always run at the MainThread,if you have 10K entities to store, the UI get stuck.
Look at this https://github.com/magicalpanda/MagicalRecord/blob/master/Docs/Saving-Entities.md

Related

objective c - for loop to fetch image from iCloud sequentially

on IOS, I need to get metadata for a selected set of images. But since the images are backed up to iCloud, sometimes it may immediately return (cached) and sometimes it may take a second or two.
The for loop runs through quickly, I am able to wait for all of the images to be processed before I move forward. But they still are being fetched in parallel. How do I make the for loop run sequentially by waiting for the block to finish before moving on to next image.
// Step 4: Fetch Details like Metadata for this batch
-(void) getDetailsForThisBatchOfNewAssets:(NSMutableArray*) mArrBatchOfNewAssets
withCompletionHandler:(blockReturnsMArrAndMArr) blockReturns{
NSLog(#"%s with arraySize of %lu",__PRETTY_FUNCTION__, (unsigned long)[mArrBatchOfNewAssets count] );
long assetCount = [mArrBatchOfNewAssets count];
NSMutableArray *mArrNewAssetsAndDetails = [[NSMutableArray alloc] init];
NSMutableArray *mArrNewAssetFailed = [[NSMutableArray alloc] init];
if(assetCount == 0){
NSLog(#" Looks like there are no NEW media files on the device.");
return;
}
else
NSLog(#"found %ld assets in all that need to be backed up", assetCount);
dispatch_group_t groupForLoopGetDetails = dispatch_group_create();
for(long i = 0 ; i < assetCount; i++){
PHAsset *currentAsset = [[mArrBatchOfNewAssets objectAtIndex:i] objectForKey:#"asset"];
NSString *mediaIdentifier = [[[currentAsset localIdentifier] componentsSeparatedByString:#"/"] firstObject];
[mArrIdentifiersInThisBatch addObject:mediaIdentifier];
dispatch_group_enter(groupForLoopGetDetails);
[mediaManager getDetailedRecordForAsset:currentAsset
withCompletionHandler:^(NSMutableDictionary *mDicDetailedRecord, NSMutableDictionary *mDicRecordForError)
{
if(mDicRecordForError[#"error"]){
[mArrNewAssetFailed addObject:mDicRecordForError];
NSLog(#"Position %ld - Failed to fetch Asset with LocalIdentifier: %#, adding it to Failed Table. Record: %#",i,[currentAsset localIdentifier], mDicRecordForError);
} else {
[mArrNewAssetsAndDetails addObject:mDicDetailedRecord ];
NSLog(#"Position %ld - added asset with LocalIdentifier to mArrNewAssetsAndDetails %#",i,[currentAsset localIdentifier]);
}
dispatch_group_leave(groupForLoopGetDetails);
}];
} // end of for loop that iterates through each asset.
dispatch_group_notify(groupForLoopGetDetails, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(#"Completed gathering details for this batch of assets for backup. Count : %lu and failed Media count: %lu",(unsigned long)[mArrNewAssetsAndDetails count], (unsigned long)[mArrNewAssetFailed count]);
blockReturns(mArrNewAssetsAndDetails,mArrNewAssetFailed);
});
}
I have looked through several questions on SO on this topic but still have not figured out how to make this run sequentially.
I don't want to do a "self call" for this method, because I'm already doing "self call" at another place before I reach this method and my code is now growing into too many notifications and catches because of that.
Assuming the completion handler of getDetailedRecordForAsset is called on a different thread, you can use a semaphore to block execution (Note: DO NOT DO this on the main thread) inside the loop while waiting for the completion handler.
Remove the dispatch group stuff, then, inside the loop:
create a semaphore right before calling getDetailedRecordForAsset like so: dispatch_semaphore_t semaphore = dispatch_semaphore_create( 0);
as the last statement of the completion handler call dispatch_semaphore_signal( semaphore);
immediately after calling getDetailedRecordForAsset, wait for the end of the completion handler with dispatch_semaphore_wait( semaphore, DISPATCH_TIME_FOREVER);
So the structure of the loop will look like:
for (assets)
{
... // get current asset, media identifier as above
dispatch_semaphore_t semaphore = dispatch_semaphore_create( 0);
[mediaManager getDetailedRecordForAsset:currentAsset
withCompletionHandler:^(NSMutableDictionary *mDicDetailedRecord, NSMutableDictionary *mDicRecordForError)
{
... // handle error or add asset details as above
dispatch_semaphore_signal( semaphore);
}
dispatch_semaphore_wait( semaphore, DISPATCH_TIME_FOREVER);
}

Core Data save and Concurrency problems in nested loops + CloudKit

I'm using CloudKit to download an array of records (contained in myArray) The myArray enumeration is within the completion handler of the CloudKit block. There are a few nested CloudKit queries and array enumerations (example below). From there, I'm creating managed objects in a loop, and saving them, which will run only on first launch and then I'd expect Core Data to have them available persistently, so the app is designed to retrieve them without the need of re-creating them.
The problem is that my objects do not appear to save, as on the apps second launch the views are empty (or that the app saves some, or it crashes), and will only fill if I re-run the code to create the objects.
I think the issue may to do with concurrency issues / threads + Core Data - which seems to be the case after adding the compiler flag suggested. Consequently, I edited my code to make use of private queues for the NSManagedObjectContext, but still have crashes. I've edited the example below to show what my code looks like now.
I've simplified the code below for clarify and the purpose of the example, but it is more or less what I have:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//Download records from CloudKit, the following enumeration is within the CloudKit completion handler
[myArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
MyManagedObj *managedObjToInsert = [NSEntityDescription
insertNewObjectForEntityForName:#"entityName"
inManagedObjectContext:self.managedObjectContext];
managedObjToInsert.property = obj[#"property"];
//Get some new records from CloudKit with predicate based on this object which is related to the new records, this next block enumeration is in the completion handler of the new query
[myNextArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
MyManagedObj *nextManagedObjToInsert = [NSEntityDescription
insertNewObjectForEntityForName:#"entityName"
inManagedObjectContext:self.managedObjectContext];
nextManagedObjToInsert.property = obj[#"property"];
nextManagedObjToInsert.relatedObj = managedObjToInsert; //relational
}];
}];
NSError *error;
if (![self.managedObjectContext save:&error])
{
NSLog(#"Problem saving: %#", [error localizedDescription]);
}
}
I've added the flag suggested in the answers below, and it seems like my managedobjectcontext is being passed outside the main thread, giving unpredictable results. Where do or how do I use the private queue blocks - assuming that is the solution to the problem?
Turn on concurrency debugging if you are worried about the threads. Add this as a command line parameter to the start up
-com.apple.CoreData.ConcurrencyDebug 1
see an example here; http://oleb.net/blog/2014/06/core-data-concurrency-debugging/
I think you are correct in worrying about threads because you can't be sure what thread your CloudKit completion block is being run. Try wrapping your object creation loop (and the save) within a [self.managedObjectContext performBlockAndWait] section.
Solved this issue using private queues with the help of the following documentation (in addition to the helpful comments/answers shared before this answer):
Correct implementation of parent/child NSManagedObjectContext
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Concurrency.html
http://benedictcohen.co.uk/blog/archives/308
The problem was that I was trying to save to the NSManagedObjectContext on the main thread whilst the code being executed by the cloudkit query to the database was occuring on another thread, resulting in crashes and inconsistent saves to the persistent store. The solution was to use the NSPrivateQueueConcurrencyType by declaring a new child context (this is only supported in iOS 5+).
Working code now looks like*:
//create a child context
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[privateContext setParentContext:[self managedObjectContext]];
//Download records from CloudKit by performing a query
[publicDatabase performQuery:myQuery inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * resultsArray, NSError * error) {
[resultsArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
__block NSManagedObjectID *myObjID;
//Async and not in main thread so requires a private queue
[privateContext performBlockAndWait:^{
MyManagedObj *managedObjToInsert = [NSEntityDescription insertNewObjectForEntityForName:#"entityOne" inManagedObjectContext:privateContext];
managedObjToInsert.property = obj[#"property"];
myObjID = managedObjToInsert.objectID;
NSError *error;
if (![privateContext save:&error]) //propergates to the parent context
{
NSLog(#"Problem saving: %#", [error localizedDescription]);
}
}];
//Get some new records from CloudKit with predicate based on this object which is related to the new records, this next block enumeration is in the completion handler of the new query
[publicDatabase performQuery:mySecondQuery inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * secondResultsArray, NSError * error) {
[secondResultsArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
[privateContext performBlockAndWait:^{
MyManagedObj *nextManagedObjToInsert = [NSEntityDescription insertNewObjectForEntityForName:#"entityTwo" inManagedObjectContext:privateContext];
nextManagedObjToInsert.property = obj[#"property"];
NSManagedObject *relatedObject = [privateContext objectWithID:myObjID];
nextManagedObjToInsert.relatedObj = relatedObject; //relational
}];
}];
}];
NSError *childError = nil;
if ([privateContext save:&childError]) { //propagates to the parent context
[self.managedObjectContext performBlock:^{
NSError *parentError = nil;
if (![self.managedObjectContext save:&parentError]) { //saves to the persistent store
NSLog(#"Error saving parent %#", parentError);
}
}];
} else {
NSLog(#"Error saving child %#", childError);
}
}];
}];
*please note that this is just an example to show how I solved the problem - there may be certain variable declarations missing, but the gist of the solution is there.

BFTask to draw SpriteKit objects in background is locking main thread

I am using BFTasks to perform some SpriteKit drawing in the background, but I'm not sure I'm using them correctly, as the drawing is locking up the main thread.
Each object is made up of several SKSpriteNodes, that are flattened before rendering. I'd like each one to render as soon as it's been flattened, i.e. when I call [self addChild:miniNode]; But it waits until all have been created, (locking the main thread) and then they appear all at once.
I've simplified my code below to show the chain of tasks:
- (void)drawLocalRelationships
{
[ParseQuery getLocalRelationships:_player.relationships block:^(NSArray *objects, NSError *error) {
[[[self drawRelationships:objects forMini:_player]
continueWithBlock:^id(BFTask *task) {
//this continues once they've all been drawn and rendered
return nil;
}];
}];
}
- (BFTask *)drawRelationships:(NSArray *)relationships forMini:(Mini *)mini
{
return [_miniRows drawSeriesRelationships:relationships forMini:mini];
}
The MiniRows class:
- (BFTask *)drawSeriesRelationships:(NSArray *)relationships forMini:(Mini *)mini
{
BFTask *task = [BFTask taskWithResult:nil];
for (Relationship *relationship in relationships) {
task = [task continueWithBlock:^id(BFTask *task) {
return [self drawRelationship:relationship mini:mini];
}];
}
return task;
}
- (BFTask *)drawRelationship:(Relationship *)relationship mini:(Mini *)mini
{
//code to determine 'row'
return [row addMiniTask:otherMini withRelationship:relationship];
}
The Row class:
- (BFTask *)addMiniTask:(Mini*)mini withRelationship:(Relationship *)relationship
{
//drawing code
MiniNode *miniNode = [self nodeForMini:mini size:size position:position scale:scale];
[self addChild:miniNode]; //doesn't actually render here
return [BFTask taskWithResult:nil];
}
I've tried running the addMiniTask method on a background thread, but it doesn't seem to make a difference. I wonder if I'm misunderstanding the concept of BFTasks - I figured they're automatically run on a background thread, but perhaps not?
BFTasks are NOT run on a background thread by default !
If you do:
BFTask * immediateTask = [BFTask taskWithResult: #"1"];
immediateTask completes, i.e. the completed property is YES, immediately in the current thread.
Also, if you do:
[task continueWithBlock:^id(BFTask *task) {
// some long running operation
return nil;
}];
Once task completes, the block is executed in the default executor, which executes blocks immediately in the current thread unless the call stack is too deep in which case it is offloaded to a background dispatch queue.
The current thread being the one where continueWithBlock is called.
So unless you're calling the previous code in a background thread, the long running operation will block the current thread.
However, you can offload a block to a different thread or queue using an explicit executor:
BFTask * task = [BFTask taskFromExecutor:executor withBlock:^id {
id result = ...; // long computation
return result;
}];
Choosing the right executor is critical:
executor = [BFExecutor defaultExecutor] the task's block is run on the current thread (the one where the task creation is performed) or offloaded to a background queue if the call stack is too deep. So it's hard to predict what will happen;
executor = [BFExecutor immediateExecutor] the task's block is run on the same thread as the previous task (see chaining below). But if the previous task was run by the default executor you don't really know which thread it is;
executor = [BFExecutor mainThreadExecutor] the task's block is run on the main thread. This is the one to use to update your UI after a long running operation.
executor = [BFExecutor executorWithDispatchQueue:gcd_queue] the task's block is run in the supplied gcd queue. Create one with a background queue to execute long running operations. The type of queue (serial or concurrent) will depend on the tasks to execute and their dependencies.
Depending on executor you will get different behaviour.
The advantage of BFTasks is that you can chain and synchronise tasks running in different threads. For example, to update the UI in the main thread after long running background operation, you would do:
// From the UI thread
BFTask * backgroundTask = [BFTask taskFromExecutor:backgroundExecutor withBlock:^id {
// do your long running operation here
id result = ...; // long computation here
return result;
}];
[backgroundTask continueWithExecutor:[BFExecutor mainThreadExecutor] withSuccessBlock:^id(BFTask* task) {
id result = task.result;
// do something quick with the result - we're executing in the UI thread here
return nil
}];
PFQuery findInBackgroundWithBlock method executes the block with the default executor, so if you call that method from the main thread, there is a great chance that the block will also execute in the main thread.
In your case, although I know nothing about SpriteKit, I would fetch all sprites, then update the UI:
- (void)queryRenderAllUpdateOnce {
NSThread *currentThread = [NSThread currentThread];
NSLog(#"current thread is %# ", currentThread);
// replace the first task by [query findObjectsInBackground]
[[[BFTask taskFromExecutor:[Tasks backgroundExecutor] withBlock:^id _Nonnull{
NSLog(#"[%#] - Querying model objects", [NSThread currentThread]);
return #[#"Riri", #"Fifi", #"LouLou"];
}] continueWithExecutor:[BFExecutor immediateExecutor] withBlock:^id _Nullable(BFTask * _Nonnull task) {
NSLog(#"[%#] - Fetching sprites for model objects", [NSThread currentThread]);
NSArray<NSString *> * array = task.result;
NSMutableArray * result = [[NSMutableArray alloc] init];
for (NSString * obj in array) {
// replace with sprite
id sprite = [#"Rendered " stringByAppendingString:obj];
[result addObject:sprite];
}
return result;
}] continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id _Nullable(BFTask * _Nonnull task) {
NSLog(#"[%#] - Update UI with all sprite objects: %#", [NSThread currentThread], task.result);
// TODO update the UI here.
return nil;
}];
}
But with this solution, all sprites are fetch (flattened ?) then the UI update. If you want to update the UI, every time a sprite is fetched, you could do something like this:
- (void)queryRenderUpdateMany {
NSThread *currentThread = [NSThread currentThread];
NSLog(#"current thread is %# ", currentThread);
[[[BFTask taskFromExecutor:[Tasks backgroundExecutor] withBlock:^id _Nonnull{
NSLog(#"[%#] - Querying model objects", [NSThread currentThread]);
return #[#"Riri", #"Fifi", #"LouLou"];
}] continueWithExecutor:[BFExecutor immediateExecutor] withBlock:^id _Nullable(BFTask * _Nonnull task) {
NSArray<NSString *> * array = task.result;
NSMutableArray * result = [[NSMutableArray alloc] init];
for (NSString * obj in array) {
BFTask *renderUpdate = [[BFTask taskFromExecutor:[BFExecutor immediateExecutor] withBlock:^id _Nonnull{
NSLog(#"[%#] - Fetching sprite for %#", [NSThread currentThread], obj);
return [#"Rendered " stringByAppendingString:obj];
}] continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id _Nullable(BFTask * _Nonnull task) {
NSLog(#"[%#] - Update UI with sprite %#", [NSThread currentThread], task.result);
return nil;
}];
[result addObject: renderUpdate];
}
return [BFTask taskForCompletionOfAllTasks:result];
}] continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id _Nullable(BFTask * _Nonnull task) {
NSLog(#"[%#] - Updated UI for all sprites", [NSThread currentThread]);
return nil;
}];
}
Here the middle task create a task that will complete once all renderUpdate tasks have completed.
Hope this help.
B

Magical Record object for current thread

I'm using Magical Record 2.3.0 beta 5 and I have troubles understanding how to get my NSManagedObjects for the current thread. I have a long running NSOperation where I need my PSPlayer (NSManagedObject).
When I init the NSOperation, I keep an id of my PSPlayer and re-fetch the same object in the operation's main method. According to Apple that the way to do it.
#implementation TAPlayerUpdateOperation
- (instancetype)initWithPlayer:(PSPlayer *)player;
{
self = [super init];
if (self) {
self.playerMD5Id = player.md5Id;
}
}
- (void)main
{
#autoreleasepool {
__block BOOL keepUpdating = YES;
PSPlayer *player = [[PSPlayer MR_findAllWithPredicate:[NSPredicate predicateWithFormat:#"md5Id == %#", self.playerMD5Id]] firstObject];
NSLog(#"player.md5Id = %#", player.md5Id);
// rest of my operation logic
}
}
#end
When I run my app with -com.apple.CoreData.ConcurrencyDebug 1, I get a crash when accessing the property in the NSLog statement.
What is the correct way to get my NSManagedObject so that it is safe for the current thread?
I've pinned the problem down to the following snippet where it crashes as well.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
PSPlayer *player =[[PSPlayer MR_findAll] firstObject];
NSLog(#"player = %#", player.name);
});
cheers,
Jan
You need to ensure that everything is saved and merged before the fetch would work. If you're using MR then it's better to take the managed object and call inContext: on it supplying the other context and have it do the work (it also avoids a predicate).
I expect the crash is because you use player.md5Id instead of self.playerMD5Id so you're accessinh the managed object on the wrong thread.

UIManagedDocument insert objects in background thread

This is my first question on Stack Overflow, so please excuse me if I'm breaking any etiquette. I'm also fairly new to Objective-C/app creation.
I have been following the CS193P Stanford course, in particular, the CoreData lectures/demos. In Paul Hegarty's Photomania app, he starts with a table view, and populates the data in the background, without any interruption to the UI flow. I have been creating an application which lists businesses in the local area (from an api that returns JSON data).
I have created the categories as per Paul's photo/photographer classes. The creation of the classes themselves is not an issue, it's where they are being created.
A simplified data structure:
- Section
- Sub-section
- business
- business
- business
- business
- business
- business
My application starts with a UIViewController with several buttons, each of which opens a tableview for the corresponding section (these all work fine, I'm trying to provide enough information so that my question makes sense). I call a helper method to create/open the URL for the UIManagedDocument, which was based on this question. This is called as soon as the application runs, and it loads up quickly.
I have a method very similar to Paul's fetchFlickrDataIntoDocument:
-(void)refreshBusinessesInDocument:(UIManagedDocument *)document
{
dispatch_queue_t refreshBusinessQ = dispatch_queue_create("Refresh Business Listing", NULL);
dispatch_async(refreshBusinessQ, ^{
// Get latest business listing
myFunctions *myFunctions = [[myFunctions alloc] init];
NSArray *businesses = [myFunctions arrayOfBusinesses];
// Run IN document's thread
[document.managedObjectContext performBlock:^{
// Loop through new businesses and insert
for (NSDictionary *businessData in businesses) {
[Business businessWithJSONInfo:businessData inManageObjectContext:document.managedObjectContext];
}
// Explicitly save the document.
[document saveToURL:document.fileURL
forSaveOperation:UIDocumentSaveForOverwriting
completionHandler:^(BOOL success){
if (!success) {
NSLog(#"Document save failed");
}
}];
NSLog(#"Inserted Businesses");
}];
});
dispatch_release(refreshBusinessQ);
}
[myFunctions arrayOfBusinesses] just parses the JSON data and returns an NSArray containing individual businessses.
I have run the code with an NSLog at the start and end of the business creation code. Each business is assigned a section, takes 0.006 seconds to create, and there are several hundred of these. The insert ends up taking about 2 seconds.
The Helper Method is here:
// The following typedef has been defined in the .h file
// typedef void (^completion_block_t)(UIManagedDocument *document);
#implementation ManagedDocumentHelper
+(void)openDocument:(NSString *)documentName UsingBlock:(completion_block_t)completionBlock
{
// Get URL for document -> "<Documents directory>/<documentName>"
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:documentName];
// Attempt retrieval of existing document
UIManagedDocument *doc = [managedDocumentDictionary objectForKey:documentName];
// If no UIManagedDocument, create
if (!doc)
{
// Create with document at URL
doc = [[UIManagedDocument alloc] initWithFileURL:url];
// Save in managedDocumentDictionary
[managedDocumentDictionary setObject:doc forKey:documentName];
}
// If the document exists on disk
if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]])
{
[doc openWithCompletionHandler:^(BOOL success)
{
// Run completion block
completionBlock(doc);
} ];
}
else
{
// Save temporary document to documents directory
[doc saveToURL:url
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success)
{
// Run compeltion block
completionBlock(doc);
}];
}
}
And is called in viewDidLoad:
if (!self.lgtbDatabase) {
[ManagedDocumentHelper openDocument:#"DefaultLGTBDatabase" UsingBlock:^(UIManagedDocument *document){
[self useDocument:document];
}];
}
useDocument just sets self.document to the provided document.
I would like to alter this code to so that the data is inserted in another thread, and the user can still click a button to view a section, without the data import hanging the UI.
Any help would be appreciated I have worked on this issue for a couple of days and not been able to solve it, even with the other similar questions on here. If there's any other information you require, please let me know!
Thank you
EDIT:
So far this question has received one down vote. If there is a way I could improve this question, or someone knows of a question I've not been able to find, could you please comment as to how or where? If there is another reason you are downvoting, please let me know, as I'm not able to understand the negativity, and would love to learn how to contribute better.
There are a couple of ways to this.
Since you are using UIManagedDocument you could take advantage of NSPrivateQueueConcurrencyType for initialize a new NSManagedObjectContext and use performBlock to do your stuff. For example:
// create a context with a private queue so access happens on a separate thread.
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
// insert this context into the current context hierarchy
context.parentContext = parentContext;
// execute the block on the queue of the context
context.performBlock:^{
// do your stuff (e.g. a long import operation)
// save the context here
// with parent/child contexts saving a context push the changes out of the current context
NSError* error = nil;
[context save:&error];
}];
When you save from the context, data of the private context are pushed to the current context. The saving is only visible in memory, so you need to access the main context (the one linked to the UIDocument) and do a save there (take a look at does-a-core-data-parent-managedobjectcontext-need-to-share-a-concurrency-type-wi).
The other way (my favourite one) is to create a NSOperation subclass and do stuff there. For example, declare a NSOperation subclass like the following:
//.h
#interface MyOperation : NSOperation
- (id)initWithDocument:(UIManagedDocument*)document;
#end
//.m
#interface MyOperation()
#property (nonatomic, weak) UIManagedDocument *document;
#end
- (id)initWithDocument:(UIManagedDocument*)doc;
{
if (!(self = [super init])) return nil;
[self setDocument:doc];
return self;
}
- (void)main
{
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
[moc setParentContext:[[self document] managedObjectContext]];
// do the long stuff here...
NSError *error = nil;
[moc save:&error];
NSManagedObjectContext *mainMOC = [[self document] managedObjectContext];
[mainMOC performBlock:^{
NSError *error = nil;
[mainMOC save:&error];
}];
// maybe you want to notify the main thread you have finished to import data, if you post a notification remember to deal with it in the main thread...
}
Now in the main thread you can provide that operation to a queue like the following:
MyOperation *op = [[MyOperation alloc] initWithDocument:[self document]];
[[self someQueue] addOperation:op];
P.S. You cannot start an async operation in the main method of a NSOperation. When the main finishes, delegates linked with that operations will not be called. To say the the truth you can but this involves to deal with run loop or concurrent behaviour.
Hope that helps.
Initially I was just going to leave a comment, but I guess I don't have the privileges for it. I just wanted to point out the UIDocument, beyond the change count offers
- (void)autosaveWithCompletionHandler:(void (^)(BOOL success))completionHandler
Which shouldn't have the delay I've experienced with updating the change count as it waits for a "convenient moment".