jkh
jkh

Reputation: 3678

Objective-C - Wait for call on another thread to return before continuing

In my iOS application, I have a database call that takes some time to complete. I have a spinner visible on the screen while this operation is taking place. I am hitting an error with the app crashing with "com.myapp failed to resume in time" so it seems like it is running the database call on the main thread, causing issues.

Current Code

-(void)timeToDoWork
{
    ...
    [CATransaction flush];

    [[DatabaseWorker staticInstance] doWork];

    //Additional UI stuff here
    ...

    if([self->myReceiver respondsToSelector:self->myMessage])
    {
        [self->myReceiver performSelector:self->myMessage];
    }
}

To get the doWork function to take place on a background thread, it looks like I can use Grand Central Dispatch:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    [[DatabaseWorker staticInstance] doWork];
});

However, how do I prevent the execution from continuing until it is complete? Should I end the method after the doWork call, and move everything below it to a new function?

Sample

-(void)timeToDoWork
{
    ...
    [CATransaction flush];

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        [[DatabaseWorker staticInstance] doWork];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self doneDoingWork];
        });
    });
}

-(void)doneDoingWork
{
    //Additional UI stuff here
    ...

    if([self->myReceiver respondsToSelector:self->myMessage])
    {
        [self->myReceiver performSelector:self->myMessage];
    }
}

Is there a better way to do this?

Upvotes: 0

Views: 2412

Answers (4)

Ivan Genchev
Ivan Genchev

Reputation: 2746

You can also use blocks. e.g..

- (void)doWorkWithCompletionHandler:(void(^)())handler {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        // do your db stuff here...
        dispatch_async(dispatch_get_main_queue(), ^{
            handler();
        });
    });
}

And then use it like that:

[[DatabaseWorker staticInstance] doWorkWithCompletionHandler:^{
    // update your UI here, after the db operation is completed.
}];

P.S. It might be a good idea to copy the handler block.

Upvotes: 2

Ievgenii
Ievgenii

Reputation: 189

Prevent execution in main thread from continuing is really bad idea. iOS will terminate your application since main thread should always work with run loop. I suggest you following way to handle your problem: Write a "Locker". Let it show some view with animated spinner and no buttons at all. When you start dispatch async operation just bring it to the front and let it work with run loop. When your async operation completes close the locker.

Upvotes: 2

dcorbatta
dcorbatta

Reputation: 1889

I think that you should use a delegate in your DatabaseWorker and the method doWork always run in background, so when the worker finish the work it tell to its delegate that the work is finished. The delegate method must be called in the main thread.

In the case that you have many objects that need to know when the DatabaseWorker finish instead to use a delegate I would use notifications.

EDIT:

In the DatabaseWorker class you need to implement the method doWork like this:

- (void) doWork{
         dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
         dispatch_async(queue, ^{
             //Do the work.
             dispatch_async(dispatch_get_main_queue(), ^{
                 [self.delegate finishWork];
             });
         });
}

And in the class that implement timeTodoWork:

-(void)timeToDoWork
{
    ...
    [CATransaction flush];

    [[DatabaseWorker staticInstance] setDelegate:self];
    [[DatabaseWorker staticInstance] doWork];
}

#pragma mark DatabaseWorkerDelegate
- (void) finishWork{
    //Additional UI stuff here
    ...

    if([self->myReceiver respondsToSelector:self->myMessage])
    {
        [self->myReceiver performSelector:self->myMessage];
    }
}

Also you can use:

[self performSelectorInBackground:@selector(doWorkInBackground) withObject:nil];

instead of:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
         dispatch_async(queue, ^{
             //Do the work.
});

And add a method:

- (void) doWorkInBackground{
       //Do the work
        [self.delegate performSelectorOnMainThread:@selector(finishWork) withObject:nil waitUntilDone:NO];
}

Upvotes: 0

FluffulousChimp
FluffulousChimp

Reputation: 9185

The error you are receiving suggests that you are doing something in application:didFinishLaunchingWithOptions: or applicationDidBecomeAction: or somewhere else in the launch cycle that is taking too long and the app is getting terminated by the launch watchdog timer. Above all, it is vital that you return as quickly as possible from these methods. I'm not sure where your code fits into the launch cycle; but this explanation seems plausible.

There are all sorts of ways to address this; but taking the lengthy process off the main queue is the first step as you noted. Without knowing more about what main queue objects (e.g. UI) depend on this database transaction, I'd say that your suggested solution is perfectly fine. That is, dispatch the work to a background queue; and on completion dispatch the remaining UI work to the main queue.

Delegates were suggested elsewhere as a solution. That's also workable although you still have to concern yourself with which queue the delegate methods get called on.

Upvotes: 0

Related Questions