Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSInvocationOperation callback too soon

I know similar questions have been asked a few times, but I'm struggling to get my head around how this particular problem can be solved. So far, everything I've done has been carried out on the main tread. I now find that I need to perform an operation which will take some time, and I want to add a HUD to my display for the duration of the operation and fade it away when the operation is complete.

After reading a lot about GCD (and getting quite confused), I decided the simplest way to go would be to call my time consuming method with an NSInvocationOperation and add it to a newly created NSOperationQueue. This is what I have:

        [self showLoadingConfirmation]; // puts HUD on screen

        // this bit takes a while to draw a large number of dots on a MKMapView            
        NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self
                                                                                selector:@selector(timeConsumingOperation:)
                                                                                  object:[self lotsOfDataFromManagedObject]];

        // this fades the HUD away and removes it from the superview
        [operation setCompletionBlock:^{ [self performSelectorOnMainThread:@selector(fadeConfirmation:) withObject:loadingView waitUntilDone:YES]; }];

        NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
        [operationQueue addOperation:operation];

I would expect this to show the HUD, start drawing the dots on the map, and then once that operation is finished, fade away the HUD.

Instead, it shows the HUD, starts drawing the dots on the map, and fades way the HUD while still drawing the dots. According to my NSLogs, there is about a quarter of a second delay before calling the method to fade the HUD. Meanwhile the drawing of the dots continues for another few seconds.

What can I do to make it wait until the drawing on the map is complete before fading away the HUD?

Thanks

EDITED TO ADD:

I'm almost getting success after making the following changes:

        NSInvocationOperation *showHud = [[NSInvocationOperation alloc] initWithTarget:self
                                                                              selector:@selector(showLoadingConfirmation)
                                                                                object:nil];

        NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self
                                                                                selector:@selector(timeConsumingOperation:)
                                                                                  object:[self lotsOfDataFromManagedObject]];

        NSInvocationOperation *hideHud = [[NSInvocationOperation alloc] initWithTarget:self
                                                                              selector:@selector(fadeConfirmation:)
                                                                                object:loadingView];

        NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];

        NSArray *operations = [NSArray arrayWithObjects:showHud, operation, hideHud, nil];
        [operationQueue addOperations:operations waitUntilFinished:YES];

Strangely, it seems to be calling timeConsumingOperation first, then showLoadingConfirmation, then fadeConfirmation. This is according to my NSLogs, which are fired within those methods.

The behaviour I'm seeing on screen is this: the dots are drawn and the map adjusts it's zoom accordingly (part of the timeConsumingOperation), then the HUD appears on screen, then nothing. All three NSLogs appear instantly, even though showLoadingConfirmation does not happen until timeConsumingOperation is complete and fadeConfirmation does not seem to happen at all.

This seems very strange, but also seems to suggest that there is a way to make something happen at the completion of timeConsumingOperation.

I tried adding this:

[operationQueue setMaxConcurrentOperationCount:1];

and also this:

[showHud setQueuePriority:NSOperationQueuePriorityVeryHigh];
[operation setQueuePriority:NSOperationQueuePriorityNormal];
[hideHud setQueuePriority:NSOperationQueuePriorityVeryLow];

but they don't seem to make any difference.

like image 374
beev Avatar asked Nov 03 '22 11:11

beev


1 Answers

Be aware of what you're actually doing when setting a completion handler for NSInvocationOperation: When such operation finishes, the following happens (From NSOperation Class Reference):

The exact execution context for your completion block is not guaranteed but is typically a secondary thread. Therefore, you should not use this block to do any work that requires a very specific execution context. Instead, you should shunt that work to your application’s main thread or to the specific thread that is capable of doing it. For example, if you have a custom thread for coordinating the completion of the operation, you could use the completion block to ping that thread.

So firstly, the block is executed on a secondary thread (so it'll enter that thread's queue, and when its turn finally comes, it just sends another job to the main thread's queue). That means it'll get mixed in such queue with other pending jobs, like the last updates for the pins that were sent over to the main queue from your timeConsumingOperation: selector.

And here comes the problem: If you don't set priorities for different jobs, there's no way to tell the order in which such jobs will be finally processed, even if you know they were sent before in time. Furthermore, in your case, the fact that your NSInvocationOperation has finished doesn't necessarily mean that all your objects have been drawn in screen by the time the block is called it only means they have been sent to the UI update thread to be processed when their turn comes.

With this in mind, and given that you don't wanna go the GCD way, (which I'd recommend to give another try as I know it's not easy at the beginning but when you pick up with it you realise it's a wonderful solution for almost all multithreading stuff you'd like to do on an iPhone) I'd create a NSOperationQueue and would send all the jobs there (the one that removes the HUD included, but with lower priority than the others). This way you make sure the removal of the HUD is processed in the main queue after all the 'pin' jobs are done, which was your first goal.

like image 154
Alejandro Benito-Santos Avatar answered Nov 15 '22 05:11

Alejandro Benito-Santos