Notre application fonctionne bien, mais nous ne gérons pas parfaitement nos appels. En effet, si vous appuyez plusieurs fois d'affilée sur le bouton New Quote, sans attendre la réponse du serveur, les résultats sont assez aléatoires... ce n'est donc pas une expérience utilisateur de qualité !
Le problème, c'est qu'on envoie plusieurs appels à la fois et en fonction du réseau, ils ne vont pas forcément revenir dans le bon ordre. Pour résoudre ce problème, nous allons faire en sorte de n'autoriser qu'un seul appel à la fois !
Utilisez une seule tâche
Si on réfléchit à la façon dont fonctionnent nos appels pour l'instant, on se rend compte qu'à chaque appel, on crée une tâche différente.
En effet, lorsqu'on appelle notre méthode static QuoteService.getQuote
, on crée un nouvel objet URLSessionTask
à chaque fois :
let task = session.dataTask(with: request) { (data, response, error) in
// (...)
}
Pour changer ça, nous allons commencer par faire de cette tâche une propriété de notre classe QuoteService
:
private var task: URLSessionDataTask?
Ensuite, je peux utiliser cette tâche dans getQuote
et getImage
comme ceci :
task = session.dataTask(with: request) { (data, response, error) in
// (...)
}
Le problème, c'est que maintenant nous utilisons une propriété dans une méthode statique, donc Xcode ne va pas être content... Qu'à cela ne tienne ! Nous allons modifier toutes nos méthodes statiques en méthodes d'instances en supprimant les mot-clés static
.
Maintenant, nous devons modifier notre appel dans le contrôleur comme ceci :
QuoteService().getQuote { (success, quote) in (...) }
On appelle la fonction getQuote
sur une instance de QuoteService
et non sur la classe directement.
Annulez une tâche
Avec ce petit travail préalable, nous allons pouvoir travailler sur une instance fixe de task
, et donc on va pouvoir annuler la tâche si une autre tâche est lancée.
On a vu qu'on pouvait lancer un appel avec la méthode resume
de URLSessionTask
. Pour l'annuler, on va utiliser la méthode cancel
.
task?.cancel()
task = session.dataTask(with: request) { (data, response, error) in
// (...)
}
task?.resume()
Si une tâche est en cours, on l'annule avant de créer puis lancer une nouvelle tâche !
Testez le résultat avec le simulateur. Avec le travail que l'on vient de faire, vous pouvez tester dans le simulateur que les appels ont maintenant bien lieu les uns après les autres.
Euh... Rien n'a changé !
Eh oui ! Je vous ai (encore ) bien eu !
Pourquoi rien n’a changé ? À cause de cette ligne :
QuoteService().getQuote { (success, quote) in (...) }
À chaque fois qu'on appuie sur le bouton et qu'on appelle la fonction getQuote
, on crée une nouvelle instance de QuoteService
. Du coup, on crée à chaque fois une nouvelle instance de task
et donc on ne peut jamais annuler la tâche en cours, car on ne travaille jamais avec la même tâche !
Pour résoudre ce problème, il faudrait que l'on travaille toujours avec la même instance de QuoteService
et pour cela, nous allons découvrir et utiliser le pattern Singleton.
Découvrez le pattern Singleton
Qu’est-ce que le pattern Singleton ?
Le pattern Singleton permet de limiter l'usage d'une classe à une seule instance. Cela veut dire que l'on ne va pas pouvoir créer plusieurs instances de la classe, on ne va pouvoir en utiliser qu'une seule !
Ce pattern est souvent utile lorsqu'on a besoin de gérer un unique objet. Par exemple, en iOS, la classe UIDevice
(qui permet notamment d'avoir des informations sur le modèle, la version d'iOS du téléphone, etc.) utilise ce pattern.
En effet, votre code est exécuté sur un appareil unique. Du coup, pour accéder aux informations stockées dans cette classe, vous n'allez pas utiliser ceci :
UIDevice()
Mais ceci :
UIDevice.current
UIDevice définit une propriété statique current de type UIDevice qui est la seule instance disponible de cette classe :
class UIDevice {
static var current = UIDevice()
}
Comment devez-vous l’utiliser ?
Nous allons faire la même chose avec notre classe QuoteService
. Nous allons définir une propriété statique de type QuoteService
:
static var shared = QuoteService()
Maintenant, nous allons protéger la classe pour empêcher la création d'autres instances. Une idée de comment faire ?
Il suffit de ne pas créer d'initialiseur. Et c'est déjà le cas, donc c'est bon ?
Bien vu ! Mais pas tout à fait exact ! Souvenez-vous, les classes ont un initialiseur par défaut qui n'a aucun paramètre. C'est cet initialiseur par défaut qui nous avait permis d'écrire ceci :
QuoteService().getQuote()
L'initialiseur par défaut est présent dans les parenthèses après QuoteService
. Nous ne pouvons pas supprimer cet initialiseur par défaut, mais nous pouvons le rendre inaccessible en dehors de la classe QuoteService
en le rendant privé :
private init() {}
Et voilà ! Vous pouvez maintenant essayer d'écrire QuoteService()
, cela ne marche plus qu'à l'intérieur de la classe QuoteService
.
On a maintenant une instance unique de notre classe, que nous allons pouvoir utiliser comme ceci :
QuoteService.shared.getQuote { (success, quote) in (...) }
Vous pouvez essayer dans le simulateur ! Si vous lancez deux appels sans attendre le retour du premier, vous aurez maintenant une alerte qui s'affiche :
Cela signifie que la première tâche a bien été annulée avant qu'une deuxième ne soit créée.
Protégez l'expérience utilisateur
Nous avons enfin réussi à ne faire qu'un seul appel à la fois ! Et c'est très bien ! Mais l'expérience utilisateur n'est pas incroyable avec cette alerte.
Nous allons faire en sorte que de toute façon, l'utilisateur ne puisse pas lancer deux appels en même temps. Et pour cela, nous allons cacher le bouton le temps de la requête, et le remplacer par un indicateur d'activité.
Rien de plus simple ! Il suffit d'utiliser la propriété isHidden
pour cacher le bouton et afficher l'indicateur, puis faire l'inverse lorsque la réponse revient :
@IBAction func tappedNewQuoteButton() {
newQuoteButton.isHidden = true
activityIndicator.isHidden = false
QuoteService.shared.getQuote { (success, quote) in
self.newQuoteButton.isHidden = false
self.activityIndicator.isHidden = true
if success, let quote = quote {
self.update(quote: quote)
} else {
self.presentAlert()
}
}
}
Et comme nous sommes des développeurs qui détestons nous répéter, on va créer une jolie méthode et écrire plutôt ceci :
@IBAction func tappedNewQuoteButton() {
toggleActivityIndicator(shown: true)
QuoteService.shared.getQuote { (success, quote) in
self.toggleActivityIndicator(shown: false)
if success, let quote = quote {
self.update(quote: quote)
} else {
self.presentAlert()
}
}
}
private func toggleActivityIndicator(shown: Bool) {
activityIndicator.isHidden = !shown
newQuoteButton.isHidden = shown
}
Et voilà ! Vous pouvez lancer votre application dans le simulateur, et maintenant vous avez une belle roue qui tourne pour signifier à l'utilisateur le chargement de la citation, et l'empêcher de lancer un deuxième appel.
En résumé
Pour annuler une tâche, vous pouvez utiliser la méthode
cancel
. Pour cela, il faut que vous ayez accès à la même instance, et que vous évitiez donc la création d'une nouvelle tâche à chaque appel.Le pattern Singleton permet d'obtenir une classe qui admet une unique instance.
Pour le créer, on crée une instance dans une propriété statique, et on rend privée l'initialisation par défaut :
class Singleton {
static var shared = Singleton()
private init() {}
}
Pour une bonne expérience utilisateur, il est conseillé d'empêcher les appels multiples en utilisant un indicateur d'activité.
Vous savez désormais gérer les requêtes concurrentes grâce au Singleton Pattern. Dans le prochain chapitre, nous reviendrons plus en détail sur un mot-clé que vous avez déjà vu, guard !