Développer des Apps iOS 8 avec Swift

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

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 Donner une note à l'article (5).

Article lu   fois.

Les deux auteur et traducteur

Site personnel

Traducteur : Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 
    let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as! UITableViewCell
     
    if let rowData: NSDictionary = self.tableData[indexPath.row] as? NSDictionary,
        // Prenez la clé artworkUrl60 pour obtenir l'URL d'une image pour la miniature de l'application
        urlString = rowData["artworkUrl60"] as? String,
        imgURL = NSURL(string: urlString),
        // Obtenez le prix formaté en chaîne, pour l'afficher dans le sous-titre 
        prixFormate = rowData["formattedPrice"] as? String,
        // Obtenez le title du morceau
        nomMorceau = rowData["trackName"] as? String {
            // Affichez le prix formaté dans le sous-titre
            cell.detailTextLabel?.text = prixFormate
            // Mettez à jour le texte de textLabel pour afficher le nom du morceau obtenu de l'API
            cell.textLabel?.text = nomMorceau

            // Commencez par configurer l'image de la cellule à partir d'un fichier statique
            // Sans cela, nous allons nous retrouver sans une prévisualisation de l'image !
            cell.imageView?.image = UIImage(named: "Blank52")

            // Si cette image se trouve déjà en cache, ne plus la recharger
            if let img = imageCache[urlString] {
                cell.imageView?.image = img
            }
            else {
                // L'image n'existe pas en cache, la télécharger
                // Nous devrions faire cela dans un thread d'arrière-plan
                let request: NSURLRequest = NSURLRequest(URL: imgURL)
                let mainQueue = NSOperationQueue.mainQueue()
                NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in
                    if error == nil {
                        // Convertir les données téléchargées en un objet UIImage
                        let image = UIImage(data: data)
                        // Stocker l'image dans notre cache
                        self.imageCache[urlString] = image
                        // Mettre à jour la cellule
                        dispatch_async(dispatch_get_main_queue(), {
                            if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
                                cellToUpdate.imageView?.image = image
                            }
                        })
                    }
                    else {
                        println("Erreur: \(error.localizedDescription)")
                    }
                })   
            }

    }
    return cell
}

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 adressehttp://jamesonquave.com/tutImg/Blank52.png (par un clic droit et « Enregistrez sous »...)

 
Sélectionnez
10.
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.

 
Sélectionnez
24.
25.
26.
if let img = imageCache[urlString] {
    cell.imageView?.image = img
}

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.

 
Sélectionnez
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue(), completionHandler: {(response,data,error) -> Void in
    if error == nil {
        // Convertir les données téléchargées en un objet UIImage
        let image = UIImage(data: data)
        // Stocker l'image dans notre cache
        self.imageCache[urlString] = image
        // Mettre à jour la cellule
        dispatch_async(dispatch_get_main_queue(), {
            if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
                cellToUpdate.imageView?.image = image
            }
        })
    }
    else {
        println("Erreur: \(error.localizedDescription)")
    }
})

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

 
Sélectionnez
38.
self.imageCache[urlString] = image

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

 
Sélectionnez
39.
40.
41.
42.
43.
dispatch_async(dispatch_get_main_queue(), {
    if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
        cellToUpdate.imageView?.image = image
    }
})

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.comhttp://jamesonquave.com/blog/developing-ios-apps-using-swift-part-5-async-image-loading-and-caching/. 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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+