• 12 hours
  • Hard

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 5/24/22

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

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 !

Example of certificate of achievement
Example of certificate of achievement