57

I was having problems modifying a view inside a thread. I tried to add a subview but it took around 6 or more seconds to display. I finally got it working, but I don't know how exactly. So I was wondering why it worked and what's the difference between the following methods:

  1. This worked -added the view instantly:
dispatch_async(dispatch_get_main_queue(), ^{
    //some UI methods ej
    [view addSubview: otherView];
}
  1. This took around 6 or more seconds to display:
[viewController performSelectorOnMainThread:@selector(methodThatAddsSubview:) withObject:otherView
    waitUntilDone:NO];
  1. NSNotification methods - took also around 6 seconds to display the observer was in the viewController I wanted to modify paired to a method to add a subview.
[[NSNotificationCenter defaultCenter] postNotificationName:
 @"notification-identifier" object:object];

For reference these were called inside this CompletionHandler of the class ACAccountStore.

accountStore requestAccessToAccountsWithType:accountType withCompletionHandler:^(BOOL granted, NSError *error) {
    if(granted) {
        // my methods were here
    }
}
Olivia Stork
  • 4,660
  • 5
  • 27
  • 40
Miguel Lomelí
  • 1,224
  • 1
  • 10
  • 17
  • When you say that `performSelectorOnMainThread:` didn't work, how did it fail? Did you get an error message? Was it a runtime error a compilation error? If you didn't get an error, how do you know that it failed? – Andrew Madsen Feb 17 '12 at 21:52
  • Is `addSubview:` the only method you used that touched UI elements, or are there others as well? – Matt Wilding Feb 17 '12 at 21:52
  • @AndrewMadsen I forgot to mention that it worked but it took around 6 or more seconds to display. – Miguel Lomelí Feb 17 '12 at 22:16
  • @MattWilding Yes it was. During my testing I tried to add a blank subview and still took around 6 seconds to display with the nsnotification and performselector on main thread. – Miguel Lomelí Feb 17 '12 at 22:17

3 Answers3

74

By default, -performSelectorOnMainThread:withObject:waitUntilDone: only schedules the selector to run in the default run loop mode. If the run loop is in another mode (e.g. the tracking mode), it won't run until the run loop switches back to the default mode. You can get around this with the variant -performSelectorOnMainThread:withObject:waitUntilDone:modes: (by passing all the modes you want it to run in).

On the other hand, dispatch_async(dispatch_get_main_queue(), ^{ ... }) will run the block as soon as the main run loop returns control flow back to the event loop. It doesn't care about modes. So if you don't want to care about modes either, dispatch_async() may be the better way to go.

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • 3
    What are modes, and when should I care about them? – ma11hew28 Jan 22 '14 at 16:05
  • 4
    @MattDiPasquale: On iOS, you can basically ignore them, the runloop typically always runs in the Default mode. On OS X there are 3 more modes that you may see, `NSConnectionReplyMode`, `NSModalPanelRunLoopMode`, and `NSEventTrackingRunLoopMode`. You can look up the docs for those if you're interested. – Lily Ballard Jan 22 '14 at 21:13
  • 3
    Note that in iOS UITrackingRunLoopMode is used for certain touch event tracking (when a UIScrollView is tracking a touch movement). – cdemiris99 Feb 18 '14 at 01:08
  • 4
    That's not true that it uses the default run loop mode. performSelectorOnMainThread is documented thusly: *"This method queues the message on the run loop of the main thread using the* ***common run loop modes*** — *that is, the modes associated with the* ***NSRunLoopCommonModes*** *constant."* – Draxillion May 24 '14 at 22:24
  • If you start a model dialog window within a main-thread dispatch_async block, on OS 10.9 scrolling via mouse wheel won't work (presumably b/c that only works in the default runloop mode). Also, uncaught exceptions will always terminate the program, which is bad in production code (only way out would be to nest a try-catch within each dispatch_async-block you write). For these reasons, we prefer performSelectorOnMainThread. We created a dispatch_async_main variant that internally calls performSelectorOnMainThread, so we can keep using the nice block syntax. – Andreas Zollmann Sep 06 '14 at 18:04
  • @AndreasZollmann "uncaught exceptions will always terminate the program, which is bad in production code" GCD explicitly documents that you should never let an exception get thrown out of your block and into GCD. Also, I *very strongly* disagree with the "is bad in production code". *Ignoring exceptions* is bad in production code. This is why iOS made the choice to abort on an uncaught exception, instead of the log-and-ignore approach of OS X. – Lily Ballard Sep 11 '14 at 23:43
  • Killing the app is usually worse for the user than letting the show go on. We catch exceptions in the main runloop of the program, show a dialog to the user that something "the programmers did not foresee" has happened, send the stacktrace to our server and continue the program. Of course we don't ignore them but analyze them and fix the bugs for future releases. Most of the time it's stuff of no consequence to the user like pre-selecting the last entry of a table and forgetting to handle the case where the underlying array is empty. – Andreas Zollmann Sep 13 '14 at 09:51
  • @AndreasZollmann: An uncaught exception can mean that all bets are off for the rest of the program. Not only could invariants in your data structures no longer hold, but it's also considered undefined behavior to throw an exception through Cocoa framework code (the frameworks do not make any attempt to maintain invariants themselves in the face of exceptions). This is basically like a less-risky memory corruption. For all you know, you're about to overwrite the user's document with garbage data because of an uncaught exception. – Lily Ballard Sep 22 '14 at 23:14
  • Sorry for letting myself in, but do you have any clue as to why I was encountering the opposite behaviour? I was trying to `dispatch_async` on the main queue a `setNeedsDisplay`, but that was taking a fair amount of time to show a picture; so I put the whole method inside `performSelectorOnMainThread` and it finally worked as expected. The call is inside the `completionHandler` of a `NSURLSessionDownloadTask`, and I guess that's part of the problem, but why? – mccc Nov 01 '14 at 14:40
  • I can't use performSelectorOnMainThread in swift... And I need to wait for the UI operation to finish, so I won't get a " deleted thread with uncommitted CATransaction"... is dispatch_sync the only solution? Ignoring all run loop modes? – Axel Zehden Apr 22 '15 at 10:33
  • @AxelZehden `dispatch_async()` is the most common way to handle this in Swift. A block dispatched that way should run in all "common" modes. – Lily Ballard Apr 24 '15 at 16:20
2

It's likely because performSelectorOnMainThread:withObject:waitUntilDone: queues the message with common run loop modes. According to Apple's Concurrency Programming Guide, the main queue will interleave queued tasks with other events from the app's run loop. Thus, if there are other events to be processed in the event queue, the queued blocks in the dispatch queue may be run first, even though they were submitted later.

This article is a superb explanation to performSelectorOnMainThread vs. dispatch_async, which also answers the above question.

Michael Dorner
  • 17,587
  • 13
  • 87
  • 117
Say2Manuj
  • 709
  • 10
  • 20
0

Did you try thePerformSelectorOnMainThread with waitUntilDone=YES

Eg:

Code:

[viewController performSelectorOnMainThread:@selector(methodThatAddsSubview:) withObject:otherView waitUntilDone:YES];

I think that might solve the issue as of why the PerformSelectorOnMainThread takes so long to respond.

Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
Ruchira Randana
  • 4,021
  • 1
  • 27
  • 24