Développer des Apps iOS 8 avec Swift

Partie 5 : Chargement asynchrone d'images et mise en cache

Jameson Quave

Traducteur

Sirus64

Cette section a été complètement remise à jour pour refléter les changements de Xcode 6.3, à partir du 16 avril 2015.

Dans les parties 1 à 4, nous nous sommes penchés sur les bases de Swift et nous avons mis en place un projet simple qui crée un TableView et le remplit avec des résultats provenant de l'API d'iTunes. Si vous ne l'avez pas encore lue, regardez la première partie.

Les commentaires et les suggestions d'amélioration sont les bienvenus, alors, après votre lecture, n'hésitez pas. Commentez .

Retrouvez toute la série « Développer des Apps iOS 8 avec Swift »

Aller à la partie 4 maintenant ->

- Titre : Développer des Apps iOS 8 avec Swift

- Auteur : Jameson Quave

- Traducteur : Sirus64

- Parution : 9 octobre 2014

- Public visé :

- Licence :

Voir également :

- Partie 1 : introduction - application basique Hello World

- Partie 2 : affichage des résultats d'une requête

- Partie 3 : les bonnes pratiques

- Partie 4 : ajouter les interactions

- Partie 6 : interaction avec des vues multiples

- Partie 7 : animations, audio et cellules Table View personnalisées

- Apprendre Swift à partir d'Objective-C

- Comprendre les optionnelles : quand et comment les utiliser ?

Sommaire

- I. Ce tableau est lent ! Accélérons-le !

- II. Syntaxe des dictionnaires

- III. Effectuer les téléchargements dans un thread en arrière-plan

- IV. Code source

- V. Vous avez une question ou un problème ?

- VI. Remerciements Developpez

I - Ce tableau est lent ! Accélérons-le !

Nous avons la fonctionnalité que nous cherchions, mais si vous l'exécutez, vous allez constater que c'est très lent ! Le problème est que les images des cellules sont téléchargées une à une à partir du thread de l'interface et elles ne sont pas mises en cache. Améliorons cela.

Commençons par ajouter un dictionnaire comme membre de notre classe SearchResultsViewController :

1. var imageCache = [String : UIImage]()

II - Syntaxe des dictionnaires

C'est la première fois que nous voyons cette syntaxe, laissez-moi donc vous donner une explication rapide.

Le type spécifié ici est [String : UIImage], qui est similaire au type NSDictionary d'Objective-C, mais qui est plus strict sur le type. Il prend comme clef un String et stocke un UIImage comme valeur.

Donc, si j'ai une image nommée Bob, liée à l'UIImage avec le nom de fichier Bob.jpg, je vais l'ajouter au dictionnaire comme ceci :

imageCache["Bob"] = UIImage(named: "Bob.jpg")

L'instruction UIImage(named: "Bob.jpg") instancie un UIImage à partir du fichier nommé Bob.jpg : c'est la syntaxe standard en Swift pour les fichiers locaux. Le dictionnaire utilise un sous-script pour définir ou récupérer ses valeurs. Donc, si je veux obtenir l'image de Bob, je peux simplement utiliser :

let imageOfBob = imageCache["Bob"]

On ajoute les parenthèses pour appeler le constructeur qui initialise le dictionnaire vide. Tout simplement, comme nous utilisons APIController(), nous avons besoin d'utiliser [String : UIImage](). (NDT On parle ici des parenthèses qui différencient le type de l'appel du constructeur.)

Nous désirons améliorer quelques points de notre méthode cellForRowAtIndexPath, notamment, nous voulons accéder aux images à partir de notre cache ou les télécharger en tâche de fond si elles n'existent pas. À la suite de chaque téléchargement, les images sont stockées en cache afin que nous puissions y accéder au besoin, et ce, sans avoir besoin de relancer un processus de téléchargement.

Commençons par déplacer l'appel de imgData en dehors de notre liaison optionnelle, vers l'intérieur du bloc qui met à jour les cellules. Nous faisons cela afin de pouvoir utiliser les données image à partir de notre cache ou de les télécharger, selon le cas. Ici, nous allons passer à l'utilisation de la méthode sendAsynchronousRequest de NSURLConnection, afin de télécharger l'image dans un thread d'arrière-plan.

1. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

2. let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as! UITableViewCell

4. if let rowData: NSDictionary = self.tableData[indexPath.row] as? NSDictionary,

5. // Prenez la clé artworkUrl60 pour obtenir l'URL d'une image pour la miniature de l'application

6. urlString = rowData["artworkUrl60"] as? String,

7. imgURL = NSURL(string: urlString),

8. // Obtenez le prix formaté en chaîne, pour l'afficher dans le sous-titre

9. prixFormate = rowData["formattedPrice"] as? String,

10. // Obtenez le title du morceau

11. nomMorceau = rowData["trackName"] as? String {

12. // Affichez le prix formaté dans le sous-titre

13. cell.detailTextLabel?.text = prixFormate

14. // Mettez à jour le texte de textLabel pour afficher le nom du morceau obtenu de l'API

15. cell.textLabel?.text = nomMorceau

17. // Commencez par configurer l'image de la cellule à partir d'un fichier statique

18. // Sans cela, nous allons nous retrouver sans une prévisualisation de l'image !

19. cell.imageView?.image = UIImage(named: "Blank52")

21. // Si cette image se trouve déjà en cache, ne plus la recharger

22. if let img = imageCache[urlString] {

23. cell.imageView?.image = img

24. }

25. else {

26. // L'image n'existe pas en cache, la télécharger

27. // Nous devrions faire cela dans un thread d'arrière-plan

28. let request: NSURLRequest = NSURLRequest(URL: imgURL)

29. let mainQueue = NSOperationQueue.mainQueue()

30. NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in

31. if error == nil {

32. // Convertir les données téléchargées en un objet UIImage

33. let image = UIImage(data: data)

34. // Stocker l'image dans notre cache

35. self.imageCache[urlString] = image

36. // Mettre à jour la cellule

37. dispatch_async(dispatch_get_main_queue(), {

38. if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {

39. cellToUpdate.imageView?.image = image

40. }

41. })

42. }

43. else {

44. println("Erreur: \(error.localizedDescription)")

45. }

46. })

47. }

49. }

50. return cell

51. }

Donc, que veut dire ce code ? Essayons de comprendre les changements…

IMPORTANT !

Avant de télécharger l'image réelle, assurons-nous de configurer une image par défaut pour la cellule. Cela est nécessaire si vous voulez insérer une vue d'image. Dans le cas contraire, les images chargées ne s'afficheront pas. Créez une image vide (ici, j'utilise une taille de 52 par 52 pixels, mais cela importe peu) et importez-la dans votre projet en faisant glisser le fichier pris à partir du finder vers votre projet dans Xcode, nommez-la « Blank52 » et indiquez à votre cellule d'utiliser cette image. Vous pouvez simplement prendre mon fichier image à cette adresse (par un clic droit et « Enregistrez sous »...)

1. cell.imageView?.image = UIImage(named: "Blank52")

Maintenant, votre application devrait être moins sujette aux plantages et affichera toujours une cellule contenant une image.

III - Effectuer les téléchargements dans un thread en arrière-plan

Commençons par vérifier notre cache d'images afin de chercher si l'image est déjà téléchargée. Nous utilisons la liaison optionnelle pour vérifier la présence de l'image en cache.

1. if let img = imageCache[urlString] {

2. cell.imageView?.image = img

3. }

Si l'image n'existe pas (et initialement c'est le cas), nous devons la télécharger. Il y a plusieurs façons de débuter un téléchargement. Précédemment, nous utilisions la méthode dataWithContentsOfFile de la classe NSData, mais ici, nous allons utiliser la méthode sendAsynchronousRequest de la classe NSURLConnection qui est plus proche du fonctionnement de notre API. La principale raison est que nous voulons exécuter plusieurs petites requêtes rapidement, et nous voulons le faire en arrière-plan.

Regardez la ligne de l'appel à la méthode statique sendAsynchronousRequest de NSURLConnection, qui prend une fonction/fermeture comme paramètre de completionHandler. Les lignes après cet appel représentent une fonction exécutée après le retour de la requête asynchrone.

1. NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue(), completionHandler: {(response,data,error) -> Void in

2. if error == nil {

3. // Convertir les données téléchargées en un objet UIImage

4. let image = UIImage(data: data)

5. // Stocker l'image dans notre cache

6. self.imageCache[urlString] = image

7. // Mettre à jour la cellule

8. dispatch_async(dispatch_get_main_queue(), {

9. if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {

10. cellToUpdate.imageView?.image = image

11. }

12. })

13. }

14. else {

15. println("Erreur: \(error.localizedDescription)")

16. }

17. })

À l'intérieur du bloc, nous recevons en retour quelques variables : response, data et error.

S'il n'existe aucune erreur, nous créons une instance d'UIImage à partir des données reçues, en utilisant le constructeur UIImage(data: data).

Puis, nous stockons notre image dans le cache d'images en utilisant son URL comme clef. Cela veut dire que nous pouvons rechercher l'image dans notre dictionnaire à partir de cette dernière à n'importe quel moment et dans n'importe quel contexte.

1. self.imageCache[urlString] = image

Ensuite, nous affectons l'image à la cellule à partir du thread qui gère l'interface (file principale).

1. dispatch_async(dispatch_get_main_queue(), {

2. if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {

3. cellToUpdate.imageView?.image = image

4. }

5. })

Vous remarquez que nous utilisons également ici la méthode cellForRowAtIndexPath(). Nous le faisons parce que parfois, la cellule que ce code affichait n'est plus visible et a été réutilisée. Donc, pour éviter de placer l'image sur la mauvaise cellule, nous l'identifions dans la tableView sur base du chemin de l'indice. Si celui-ci est nil, alors la cellule n'est plus visible et nous ne devons plus la mettre à jour.

Maintenant, essayons d'exécuter notre application pour observer notre implantation rapide et souple du tableView.

IV - Code source

Le code complet est disponible sur le dépôt Github dans la branche « Part5 ».

La partie 6 se concentre sur l'ajout d'un nouveau contrôleur de vues que nous pouvons afficher et qui charge des données provenant d'iTunes.

V - Vous avez une question ou un problème ?

Rejoignez-nous sur nos forums.

VI - Remerciements Developpez

Nous remercions Jameson Quave de nous avoir aimablement autorisés à publier son article, dont le texte original peut être trouvé sur jamesonquave.com. Nous remercions aussi Sirus64 pour sa traduction, LeBzul pour sa relecture technique ainsi que jacques_jean et ClaudeLELOUP pour leur relecture orthographique et Winjerome pour sa gabarisation.

Aller à la partie 6 maintenant ->

Retrouvez toute la série « Développer des Apps iOS 8 avec Swift »