• 12 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 26/02/2020

Gérez les requêtes concurrentes avec le singleton pattern

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

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 !

Utiliser 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.

Annuler 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 !

La dure confrontation avec le réel...

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 à changer ? À 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.

Le pattern Singleton

Définition

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()
}

Utilisation

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'interface

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 donc évitez 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é.

Dans le prochain chapitre, vous allez découvrir une fonctionnalité toute simple de Swift, mais qui va rendre votre code bien plus propre : le mot-clé guard !

Exemple de certificat de réussite
Exemple de certificat de réussite