UIManagedDocument insert objects in background thread
Asked Answered
F

2

5

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.

Forgiving answered 11/6, 2012 at 15:59 Comment(0)
B
11

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.

Balalaika answered 12/6, 2012 at 10:53 Comment(3)
You are an absolute legend! I have used your first solution, as to me it seemed the simpler to understand. Unfortunately I can't give you the upvote you so deserve for this as I've only just joined, but when I can I will. I will look into NSOperations at a later date, as that confused me a little bit. Once again, many thanks for your help!Forgiving
Just for completion: Don't forget to tell your UIManagedDocument that it is in a dirty state after your (and every other) operation. So force a save by calling [document updateChangeCount:UIDocumentChangeDone]. This is what's missing in the CS193P Stanford course.Geothermal
I don't get the context.parentContext = context to "insert the context into the current hierarchy". Did you really mean to set the context as its own parent? If not should the parent be the UIManagedDocument's MOC? And in that case, how do we know what concurrency type that one uses?Hornmad
M
0

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

Millennium answered 27/7, 2012 at 2:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.