Exibindo telas como resultados de requests, sem atropelamentos
Essa semana, eu conversei com outro dev iOS sobre um problema clássico durante o desenvolvimento que, com certeza, se você já participou de um processo seletivo pra uma vaga de dev mobile iOS, você acabou enfrentando.
“Como executar um processo após uma request, um de cada vez, quando suas request rodam em paralelo?”
Foi aí então que surgiu a ideia para esse post.
Vamos ver aqui como conseguimos resolver essa situação.
GCD – The unglorious saviour
Muita gente teme essas letrinhas mais do que qualquer coisa quando precisa fazer algo assincronamente no iOS.
Por quê?
Se usado da forma correta, o Grand Dispatch Central pode te salvar e proporcionar uma experiencia incrível pro usuário.
Se usado da forma incorreta, pode te levar a loucura, com erros praticamente “indebugáveis”, cenários quase impossíveis de serem reproduzidos, memory corruption e o caos.
É um salvador sem glória: irá te tirar do desastre, mas poderá te levar a um muito pior.
O GCD vai muito além dos famosos asyncs que os devs usam para mandar processamento para background ou trazer o processamento de volta à main thread. Ele possui recursos muito úteis para diversos tipos de situações, como os DispatchGroups, DispatchSemaphores, QoS para Global Threads; mas hoje eu quero falar um pouco sobre apenas um desses recursos: DispatchSemaphores.
Imagine a situação em que um processo pode ser executado várias vezes, mas apenas uma execução por vez.
Seria simples se pensarmos em um ambiente mono thread. O processamento terminaria, e seria executado novamente.
Mas em um ambiente multi-thread, isso se torna um problema. Como saber se o processamento terminou para começar o próximo? Como fazer o processamento esperar para começar o próximo, se o anterior ainda estiver em execução?
É aí que entram as filas e semáforos.
Filas e Semáforos
Primeiro, precisamos criar uma fila em background para realizar a execução do nosso processo, que no nosso caso, será criar e exibir uma modal no nosso app.
Para isso, criamos uma DispatchQueue. Ao criar uma Queue, temos basicamente duas opções de criação, baseadas na forma como a Queue irá executar as taks que ela receber:
Serial: uma queue de execução em fila (FIFO), onde um processamento é executado por vez, na ordem de entrada.
Concurrent: uma queue de execução concorrente, onde cada processamento é executado em sua própria thread, e elas rodam paralelamente.
No nosso caso, iremos criar uma fila Serial, pois nossa execução será baseada no ordem de entrada, uma por vez.
private let dispatchQueue = DispatchQueue(label: "dev.andresalla.DispatchTest.ModalPresenter")
Com isso resolvemos nosso problema de abertura de modals, certo?
ERRADO.
Como não estamos na main thread, enviamos a abertura da modal assincronamente para a main thread. Após esse Dispatch, a execução do nosso processo é encerrado, e o próximo da fila entra em execução.
Como então faremos para a próxima execução aguardar o fechamento da modal? Simples: usaremos um semáforo.
Semáforos permitem aguardar (wait) até que ele esteja com recursos livres para continuar.
Quando você cria um semáforo, você especifica a quantidade de “recursos” que ele possui, ou seja, a quantidade de vezes quele pode passar pelo wait antes que tenha que esperar algum recurso ser liberado (signal) para prosseguir com a sua execução.
Dessa forma, criamos nosso semáforo para um recurso por vez, e pedimos que ele aguarde na nossa fila (wait), até que a modal seja encerrada.
private let dispatchSemaphore = DispatchSemaphore(value: 1)
Mas como vamos saber se a modal foi fechada para podermos liberar o recurso?
Vamos criar um callback!
Um callback pra chamar de seu
Nosso callback irá ser chamado na modal, no momento do dismiss, para podermos liberar o recurso do nosso semáforo.
Para isso, criamos um Protocolo ModalPresentable, com uma variável dismissAction, que irá ser executada no momento do dismiss da modal.
Quando colocarmos nossa modal na fila para apresentação, vamos pedir para que ela seja um UIViewController e conforme o protocolo ModalPresentable.
Dessa forma, podemos implementar o release (signal) do nosso recurso com o semáforo.
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
E está feito! Nossas telas serão exibidas de modo sequencial, sem atropelamentos, e serão exibidas quando a sua antecessora (se tiver) for fechada.
Espero que esta dica seja útil pra você!! Em breve tem mais!