Showing modals as a result for a requests (look at the semaphores)
Last week, I talked to another iOS developer about a classic problem encountered during development that, for sure, if you have participated in a selection process for an iOS mobile developer job, you have likely faced.
“How do you execute a process after a request, one at a time, when your requests run in parallel?”
It was then that the idea for this post came to mind.
Let’s see now how we managed to resolve this situation.
GCD – The inglorious saviour
Many people fear these letters more than anything when they need to do something asynchronously on iOS.
Why?
If used correctly, Grand Central Dispatch (GCD) can save you and provide an incredible user experience.
If used incorrectly, it can drive you crazy with practically “undebuggable” errors, almost impossible to reproduce scenarios, memory corruption, and chaos.
It is an inglorious saviour: it can get you out of disaster, but it can also take you to a much worse one.
GCD goes far beyond the famous asyncs that developers use to send a process to a background thread or bring processing back to the main thread. It has very useful resources for different types of situations, such as DispatchGroups, DispatchSemaphores, and QoS for Global Threads; but today I want to talk a little bit about just one of these features: DispatchSemaphores.
Imagine a situation where a process can be run multiple times, but only one run at a time.
It would be simple if we think of a single-threaded environment. Processing would end, and run again.
But in a multi-threaded environment, this becomes an issue. How do I know if the processing is finished to start the next one? How do I make processing wait to start the next one if the previous one is still running?
That’s where queues and semaphores come in.
Queues and Semaphores
First, we need to create a queue in the background to carry out the execution of our process, which in our case will be to create and display a modal in our app.
For this, we created a DispatchQueue. When creating a Queue, we basically have two creation options, based on how the Queue will execute the tasks it receives:
Serial: a queue-running execution (FIFO), where one process is performed at a time, in the order of entry.
Concurrent: a concurrent execution queue, where each process runs on its own thread, and they run in parallel.
In our case, we’ll create a Serial queue, as our execution will be based on the input order, one at a time.
private let dispatchQueue = DispatchQueue(label: "dev.andresalla.DispatchTest.ModalPresenter")
So we solved our problem of opening modals, right?
WRONG.
Since we aren’t on the main thread, we send the modal opening asynchronously to the main thread. After this dispatch, the execution of our process ends, and the next one in the queue starts executing.
How do we make it wait for the next execution until the modal closes? Simple: we’ll use a semaphore.
Semaphores allow you to wait (wait method) until they have free resources to continue.
When you create a semaphore, you specify the number of “resources” it has, that is, the number of times it can pass through the wait method before it has to wait for some resource to be released (signal method) to proceed with its execution.
In this way, we create our semaphore to allow one resource at a time, and ask it to wait in our queue (wait) until the modal is closed.
private let dispatchSemaphore = DispatchSemaphore(value: 1)
But how do we know if the modal has been closed so we can release the resource?
Let’s create a callback!
One callback to rule them all
Our callback will be called in the modal, at dismiss, so that we can release the resource of our semaphore.
For this, we created a ModalPresentable Protocol, with a dismissAction variable, which will be executed when the modal is dismissed.
When we put our modal in the queue for presentation, we will require it to conform to UIViewController and ModalPresentable.
In this way, we can implement the release (signal) of our resource with the semaphore.
protocol ModalPresentable: AnyObject {
var dismissAction: (() -> Void)? { get set }
}
class ViewController: UIViewController {
private let dispatchQueue = DispatchQueue(label: "dev.andresalla.DispatchTest.ModalPresenter")
private let dispatchSemaphore = DispatchSemaphore(value: 1)
// Outras variáveis e constantes
func presentModal(_ viewController: UIViewController & ModalPresentable) {
dispatchQueue.async { [weak self] in
self?.dispatchSemaphore.wait()
DispatchQueue.main.async {
viewController.dismissAction = { [weak self] in
self?.dispatchSemaphore.signal()
}
self?.present(viewController, animated: true, completion: nil)
}
}
}
// Outros métodos
}
Run baby, run
And it’s done! Our screens will be displayed sequentially, without overlapping, and will be displayed when its predecessor (if any) is closed.
I hope this tip is useful for you! More to come soon!