UIAlertController sometimes does not call its UIAlertAction handler

The iOS app that I’m helping to develop displays the following behavior, observed on an iPad Pro (4th generation) running iOS 18.1.1:

The app uses UIAlertController to show an action sheet with two buttons (defined by two UIAlertAction objects). Each button has a handler block, and the first thing each handler does is to log that it was called.

When the user taps one of the buttons and the action sheet disappears, most of the time the appropriate UIAlertAction handler is called. But sometimes there is no log entry for either of the action handlers, nor does the app do anything else associated with the chosen button, in which case I conclude that the handler was not called.

I want to emphasize that I’m describing instances of the same action sheet displayed by the same code. Most of the time the appropriate button handler is called, but sometimes the handler is not called.

The uncalled handler problem occurs about once per hour during normal use of the app. The problem has continued to occur across many weeks of testing.

What could cause UIAlertController not to call its action handler?

Does your app tinker with the run loops as part of your alert presentation?

— Ed Ford,  DTS Engineer

Ed Ford, Apple DTS Engineer asked:

Does your app tinker with the run loops as part of your alert presentation?

No, a search in our code for the string “runloop” (case insensitive) finds matches only in two third-party components that are not involved in the alert presentation:

https://github.com/jdg/MBProgressHUD/blob/master/MBProgressHUD.m

https://github.com/MortimerGoro/MGSwipeTableCell/blob/master/MGSwipeTableCell/MGSwipeTableCell.m

Here are a few more details about our alert presentation function in case they are relevant:

  • The function and its callers are written in Objective-C.

  • The function must be called on a secondary thread. It enforces this by verifying at the top of the function that NSThread.isMainThread is false.

  • There is a local variable called “response”, of an enum type, to store which alert action the user chose. It is initialized to a value meaning “unknown”.

  • The function calls dispatch_async(dispatch_get_main_queue(), ...) with a block that creates and presents the alert. The action handlers all contain code to set the “response” variable.

  • The function then waits for a user response to the alert. Originally the wait code used a GCD semaphore (the alert action handlers called dispatch_semaphore_signal). When we noticed the uncalled handler problem, we tried switching to a POSIX semaphore. Then we tried avoiding semaphores altogether, instead using a polling loop that checks every 100ms (via usleep) to see if the “response” variable is set. The uncalled handler problem has persisted under all three methods of waiting for a user response.

  • If and when an action handler is called and the “response” variable is set, the alert presentation function returns the value of the “response” variable.

I've only seen the issue you describe when people tinker with the main thread's run loop, but what you're doing with semaphores and usleep is conceptually similar. Rather than having something poll for the result on the background thread, what happens if you instead have a method that the alert can report the result to, so that handling of the result kicks off then, rather than sitting and polling forever?

— Ed Ford,  DTS Engineer

To be clear, even when the alert action handler doesn’t get called, we do see subsequent logging from other functions that run on the main thread, such as gesture handlers, applicationWillResignActive, etc. So we know that the main thread has not hung. Is this consistent with the other instances of this issue you’ve seen?

I will certainly use a callback if I have to. The alert presentation function is used in about a dozen different places; we’ve seen the uncalled handler problem only for the most frequently shown action sheet, but if I understand you correctly the same problem could occur anywhere the function is used.

The motivation for semaphores/polling was that, like the old NSAlert runModal function, it allows the caller to have straightforward linear code. (Using async/await was not an option because using Objective-C rather than Swift was a requirement of the project.)

A possibly related WWDC video says, “A common anti-pattern is trying to make an asynchronous API act synchronous by waiting on a semaphore. This should always be avoided on the main thread.”

https://developer.apple.com/videos/play/wwdc2021/10258?time=550

I thought this meant that the main thread should never wait on a semaphore. Should the main thread also never signal a semaphore, as in the alert case? What if both the waiting thread and the signaling thread are secondary threads? I’m asking because there are a few other places where our code uses a semaphore “to make an asynchronous API act synchronous,” always in an effort to allow linear code.

There is a local variable [...] to store which alert action the user chose.

Do you mean it’s a local variable on the caller’s stack, or is it actually a global/static variable, or something else? How does the alert presenting function access it? If it’s currently a local stack variable and is captured by the async block, can you test by changing it to a global/static?

UIAlertController sometimes does not call its UIAlertAction handler
 
 
Q