TL;DR: Don’t run modal sessions inside of
dispatch_async calls or you’ll see poor scrolling performance.
If you search around the web for differences between scheduling deferred work with
dispatch_async/dispatch_after versus the
-performSelector:... family of methods, you’ll notice most people simply consider it a matter of style. There are, however, subtle differences in how these APIs work that can significantly impact how and when your code is scheduled to execute. Let’s start by reviewing how run loops work.
In its simplest form, a run loop simply spins indefinitely, blocking while waiting for events and processing any events as they come in. In pseudocode, it looks something like this:
On macOS, timers, mouse events, and keyboard events are some examples of work that interrupt the main run loop; on iOS, one example is touch events. This is why it’s important to keep expensive work off the main thread – it prevents the run loop from processing events that drive user interaction.
Deep down, things are a bit more complicated than the pseudocode shown above. For one, run loops can run in different modes, and a run loop will only process events scheduled with the mode it’s running in.
To explain that better, let’s walk through an example. A repeating
NSTimer created with one of the
+scheduledTimer... factory methods is scheduled with the
NSDefaultRunLoop mode by default. If you pull down a menu on macOS while that timer is scheduled, you’ll notice timer events don’t get delivered as long as the menu is up. This happens because the menu tracking code spins its own run loop in the
NSEventTrackingRunLoop mode, which the timer is not scheduled with. This causes the delivery of timer events to be postponed until the menu is dismissed. If you wish to receive timer events while a menu is pulled down, you can simply schedule the timer with
NSCommonRunLoopModes, which includes the
NSEventTrackingRunLoop mode in addition to the
Now timer events will be processed even while the menu is pulled down.
Similarly, on iOS, if you use the now-deprecated
NSURLConnection class, you may find yourself not getting delegate method calls while you’re scrolling. This is, again, because scrolling occurs in a different run loop mode which your
NSURLConnection, by default, is not scheduled with. This causes URL connection events to get deferred until scrolling stops. In most cases this is desirable behavior as it improves scrolling performance.
Recently, I’ve noticed an issue where table view scrolling would get extremely unresponsive in modal windows in two of my ongoing projects. After looking further, I noticed that in both of these cases, the modal session starts inside of a
If we simply switch to using
-performSelector:withObject:afterDelay:, scrolling is responsive again:
As you probably guessed by now, the issue boils down to which run loop modes our work is scheduled with. While undocumented,
dispatch_async executes work in
NSRunLoopCommonModes, which on macOS include the
NSEventTrackingRunLoop modes. This means while we’re in
dispatch_async, our modal session is interferering with events scheduled with the
performSelector:... call, however, executes work in the
NSDefaultRunLoop mode (and this is actually documented). This means any event tracking code scheduled with the
NSEventTrackingRunLoop mode is unaffected, and table view scrolling is buttery-smooth again.