Développer des Apps iOS 8 avec Swift

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

Jameson Quave

Traducteur

bredelet

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

Dans les parties 1 à 6, nous avons vu des notions de base de Swift et nous avons fait un projet qui crée une Table View et y insère des résultats issus de l'API iTunes. Si vous n'avez pas encore vu cela, voici la Partie 1.

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 6 maintenant ->

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

- Auteur : Jameson Quave

- Traducteur : bredelet

- Parution : 14 février 2015

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Jameson Quave - Developpez.com. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

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 5 : chargement asynchrone d'images et mise en cache

- Partie 6 : interaction avec des vues multiples

- Apprendre Swift à partir d'Objective-C

- Comprendre les optionnelles : quand et comment les utiliser ?

Sommaire

- I. Introduction

- II. Mettre en place notre API Controller

- III. Configurer la vue de détail

- IV. Ajouter une cellule personnalisée de Table View

- V. Sonner la musique

- VI. Ajouter des animations

- VII. Code source

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

- IX. Remerciements Developpez

I - Introduction

Dans ce tutoriel, nous allons implémenter une vue détails de l'album qui envoie une seconde requête à l'API pour récupérer la liste des pistes de l'album, télécharge les illustrations à haute résolution et permet de jouer les aperçus des pistes depuis notre application. En bonus et en option, nous allons aussi ajouter de chouettes animations à l'aide de l'API Core Animation qui est fournie avec le SDK iOS. Voici un aperçu de ce que cela donnera quand nous aurons fini (vidéo capturée depuis le simulateur iOS) :

Animation en format "youtube" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://www.youtube.com/watch?v=9Jxg_b2t2lY

II - Mettre en place notre API Controller

Puisque dans cette partie du tutoriel nous allons ajouter de nouveaux appels à l'API, c'est le moment de réviser notre API Controller pour permettre la réutilisation du code. Commençons avec une requête get plus générique.

Dans votre API Controller, ajoutez la fonction get() qui prend en entrée une chaîne de caractères path et la convertit en NSURL :

1. func get(path: String) {

2. let url = NSURL(string: path)

3. ...

Puis, récupérez la NSURLSession et envoyez-la à l'aide de dataTaskWithURL comme nous l'avions fait auparavant. En fait, le code est une réplique exacte de ce que nous avons dans notre fonction searchItunesFor(), alors faites un couper-coller depuis là-bas. Commencez votre sélection juste après la ligne

1. let urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music&entity=album"

Et faites migrer le tout dans la méthode get(). Votre fichier APIController.swift devrait ressembler maintenant à ceci :

1. import Foundation

3. protocol APIControllerProtocol {

4. func didReceiveAPIResults(results: NSArray)

5. }

7. class APIController {

8. var delegate: APIControllerProtocol

10. init(delegate: APIControllerProtocol) {

11. self.delegate = delegate

12. }

14. func get(path: String) {

15. let url = NSURL(string: path)

16. let session = NSURLSession.sharedSession()

17. let task = session.dataTaskWithURL(url!, completionHandler: {data, response, error -> Void in

18. println("Tâche terminée")

19. if(error != nil) {

20. // S'il y a une erreur dans la requête Web, l'afficher dans la console

21. println(error.localizedDescription)

22. }

23. var err: NSError?

24. if let jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as? NSDictionary {

25. if(err != nil) {

26. // S'il y a une erreur dans l'interprétation du JSON, l'afficher dans la console

27. println("Erreur JSON \(err!.localizedDescription)")

28. }

29. if let results: NSArray = jsonResult["results"] as? NSArray {

30. self.delegate.didReceiveAPIResults(results)

31. }

32. }

33. })

34. // task est juste un objet avec toutes ces propriétés définies

35. // Afin d'exécuter réellement la requête Web, nous devons appeler resume()

36. task.resume()

37. }

39. func searchItunesFor(searchTerm: String) {

40. // L'API iTunes demande des termes multiples, séparés par des + , alors remplacez les espaces par des +

41. let itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+", options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)

43. // Maintenant, échappez tout ce qui n'est pas URL-friendly

44. if let escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding) {

45. let urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music&entity=album"

46. }

47. }

49. }

Maintenant, dans notre fonction searchItunesFor(), il ne nous reste qu'à appeler la nouvelle fonction get() et la fonction sera réduite à sa plus simple expression. Ajoutez un appel de méthode get(urlPath) à la fin. La méthode finale devrait ressembler à :

1. func searchItunesFor(searchTerm: String) {

3. // L'API iTunes demande des termes multiples, séparés par des + , alors remplacez les espaces par des +

4. let itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+", options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)

6. // Maintenant, échappez tout ce qui n'est pas URL-friendly

7. if let escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding) {

8. let urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music&entity=album"

9. get(urlPath)

10. }

11. }

Vous voyez la différence ? La seule partie spécifique à la fonction de recherche d'album était la façon d'échapper les termes de la recherche et de les placer dans l'URL, alors il n'y a aucune raison de ne pas factoriser le code du get() dans sa propre méthode.

Nous pouvons maintenant ajouter vite fait une autre fonction API pour accéder à un album spécifique. Mais premièrement, modifions notre modèle d'album pour y sauver une valeur utilisée par iTunes pour identifier des albums particuliers.

Dans notre classe Album, ajoutez une nouvelle variable collectionId de type Int.

1. let collectionId: Int

Éditez le constructeur pour prendre un collectionId comme argument et ajoutez une ligne pour initialiser collectionId en même temps que les autres variables passées à init().

init(nom: String, prix: String, miniatureImageURL: String, largeImageURL: String, itemURL: String, artisteURL: String, collectionId: Int) {
self.titre = nom
self.prix = prix
self.miniatureImageURL = miniatureImageURL
self.largeImageURL = largeImageURL
self.itemURL = itemURL
self.artisteURL = artisteURL
self.collectionId = collectionId
}

Parfait ! Nous sommes maintenant capables d'initialiser un Album avec un collectionId, mais du coup notre code de albumsWithJSON est erroné, il lui manque le paramètre collectionId.

Identifiez la ligne qui crée le newAlbum juste avant de l'ajouter à la liste retournée par albumsWithJSON().

Modifiez-la pour extraire collectionId du dictionnaire retourné par la fonction et passez-le en paramètre du constructeur de l'Album. Pour que cette application fonctionne, nous avons vraiment besoin que collectionID ne soit pas nil, nous allons donc regrouper la création de tout l'album à l'intérieur d'une clause if let, de sorte que seuls les albums valides apparaîtront sur la liste.

1. if let collectionId = result["collectionId"] as? Int {

2. var newAlbum = Album(nom: nom!,

3. prix: prix!,

4. miniatureImageURL: miniatureURL!,

5. largeImageURL: imageURL!,

6. itemURL: itemURL!,

7. artisteURL: artisteURL!,

8. collectionId: collectionId!)

9. albums.append(newAlbum)

10. }

La raison pour laquelle nous avons besoin de cette variable collectionId est de nous permettre de quérir des albums quand ils sont sélectionnés. Il est simple de faire une seconde requête à l'API iTunes avec le collectionId pour récupérer une flopée de détails sur un album particulier. Par exemple, nous pouvons obtenir une liste de pistes avec des URL de ressources multimédias qui offrent un aperçu de 30 secondes.

III - Configurer la vue de détail

Dans le dernier tutoriel, nous avions ajouté un DetailsViewController à notre storyboard. Ajoutons aussi un TableView à cette vue. Vous pouvez l'arranger comme il vous plaît, mais je recommande de dédier la plus grande partie de l'affichage au TableView. C'est là que nous allons charger les pistes.

Maintenant, connectons ce nouveau TableView à une propriété dans le DetailsViewController appelée pistesTableView.

1. @IBOutlet weak var pistesTableView: UITableView!

Puis, choisissons le DetailsViewController comme dataSource et délégué du TableView, et implémentons le protocole comme auparavant :

1. func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

2. return 0

3. }

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

6. return UITableViewCell()

7. }

C'est probablement une bonne idée à ce point d'essayer de faire tourner l'application. Vous devriez pouvoir cliquer jusqu'aux détails d'un album et voir une liste vide de pistes.

Tout marche ? C'est bien, continuons…

Pour permettre d'afficher les pistes, il va nous falloir un autre modèle. Créez un nouveau fichier Swift appelé Track.swift et donnez-lui trois propriétés de type String pour le titre, le prix et la previewUrl.

1. import Foundation

2. struct Track {

3. let titre: String

4. let prix: String

5. let previewUrl: String

7. init(titre: String, prix: String, previewUrl: String) {

8. self.titre = titre

9. self.prix = prix

10. self.previewUrl = previewUrl

11. }

12. }

Ce modèle a presque exactement la même configuration que celui de l'album, rien de neuf ici.

Dans DetailsViewController, ajoutons une liste de pistes en tant que nouvelle propriété.

1. var pistes = [Track]()

Maintenant, pour obtenir les informations pour les pistes de l'album, nous devons modifier encore une fois notre API Controller. Par chance, nous avons une fonction get() simple d'utilisation, qui rend les choses faciles.

Ajoutons une nouvelle fonction à APIController qui prend un entier collectionId comme argument, et faisons-lui appeler get() pour obtenir les informations de la piste.

1. func lookupAlbum(collectionId: Int) {

2. get("https://itunes.apple.com/lookup?id=\(collectionId)&entity=song")

3. }

Nous aurons besoin de cela dans notre DetailsViewController, alors nous devons implémenter l'APIControllerProtocol que nous avons écrit plus tôt dans le DetailsViewController. Ainsi, modifiez la définition de la classe DetailsViewController pour inclure ça, et aussi notre objet API.

1. class DetailsViewController: UIViewController, APIControllerProtocol {

2. lazy var api : APIController = APIController(delegate: self)

3. ...

Votre projet aura à ce point une erreur à cause du protocole que nous n'avons pas encore mis en œuvre, mais ce n'est pas grave, continuons.

Dans la méthode viewDidLoad de DetailsViewController, nous voulons ajouter une portion pour télécharger les pistes en fonction de l'album sélectionné, alors insérons le code suivant :

1. // Charger les pistes

2. if self.album != nil {

3. api.lookupAlbum(self.album!.collectionId)

4. }

Nous avons déjà vu ce genre de chose avant. Nous créons une instance de notre APIController avec self comme délégué, et nous utilisons notre nouvelle méthode lookupTrack pour rechercher les détails des pistes de l'album sélectionné. Ici, nous utilisons le mot-clé lazy pour indiquer que nous ne voulons pas que APIController soit instancié avant son utilisation. Nous devons éviter la dépendance circulaire du cas de figure où DetailsViewController aurait besoin d'être initialisé pour le passer en argument au constructeur APIController(delegate:). Précédemment, nous avons utilisé un APIController optionnel pour résoudre ce problème, mais l'utilisation du mot-clé lazy en est une autre façon, plus propre.

Pour adhérer strictement à notre protocole APIControllerProtocol, nous devons aussi implémenter la fonction didReceiveAPIResults() dans cette classe. Nous allons l'utiliser pour charger les données de la piste. Nous l'implémenterons exactement comme nous l'avons fait pour le SearchResultsViewController, en déléguant à Track la charge de convertir la réponse JSON en liste de pistes.

1. // MARQUE: APIControllerProtocol

2. func didReceiveAPIResults(results: NSArray) {

3. dispatch_async(dispatch_get_main_queue(), {

4. self.pistes = Track.tracksWithJSON(results)

5. self.pistesTableView.reloadData()

6. UIApplication.sharedApplication().networkActivityIndicatorVisible = false

7. })

8. }

Nous utilisons une méthode inexistante tracksWithJSON() de Track. Donc, nous devons ajouter ça avant de pouvoir compiler. Ouvrez Track.swift et ajoutez une méthode modelée sur notre méthode albumsWithJSON.

1. static func tracksWithJSON(results: NSArray) -> [Track] {

2. var pistes = [Track]()

3. for pisteInfo in results {

4. // Créer la piste

5. if let type = pisteInfo["kind"] as? String {

6. if type == "song" {

7. var prix = pisteInfo["trackPrice"] as? String

8. var titre = pisteInfo["trackName"] as? String

9. var previewUrl = pisteInfo["previewUrl"] as? String

10. if(titre == nil) {

11. titre = "Inconnu"

12. }

13. else if(prix == nil) {

14. println("Aucun prix disponible en \(pisteInfo)")

15. prix = "?"

16. }

17. else if(previewUrl == nil) {

18. previewUrl = ""

19. }

20. var piste = Track(titre: titre!, prix: prix!, previewUrl: previewUrl!)

21. pistes.append(piste)

22. }

23. }

24. }

25. return pistes

26. }

Cet appel d'API retourne l'album avant de retourner la liste de pistes, alors nous devons mettre un test à la ligne 6, pour nous assurer que la clef "type" a la valeur "song". Si nous ne le faisons pas, la fonction ne fera qu'extraire certaines données de la chaîne JSON. Ensuite, nous regardons si les trois attributs dont nous avons besoin ne valent pas nil, et si c'est le cas, nous leur donnons des valeurs raisonnables par défaut.

Dans DetailsViewController, modifions maintenant la propriété numberOfRowsInSection pour retourner le nombre de pistes :

1. func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

2. return tracks.count

3. }

Puis éditons notre méthode cellForRowAtIndexPath pour charger nos données de la piste.

D'abord, nous devons ajouter un prototype de cellule à TableView dans notre storyboard, car nous allons utiliser une cellule personnalisée.

Alors, sélectionnez la vue TableView dans le storyboard, et choisissez 1 comme nombre de prototypes de cellules. Ensuite, sélectionnez la cellule elle-même et entrez « TrackCell » comme Identifier dans l'inspecteur des attributs (sur le panneau de droite quand Table View Cell est sélectionné).

IV - Ajouter une cellule personnalisée de Table View

Pour montrer à quoi servent vraiment les prototypes de cellules, je pense que nous devrions ajouter des contrôles personnalisés aux nôtres. Créez une nouvelle classe Swift appelée TrackCell qui hérite de UITableViewCell, et donnez-lui deux IBOutlets de type UILabel appelés playIcon et titreLabel.

Retournez au fichier du storyboard. Changez sa classe en TrackCell sous l'inspecteur d'identité dans le panneau de droite.

Ensuite, ajoutez deux UILabel à la cellule en glissant les vues sur la cellule même. Placez-en un sur la gauche pour notre bouton lecture/pause, et un autre occupant l'espace de droite pour dire le titre de la piste.

Faites glisser deux labels sur le prototype de cellule. Le premier sera petit et aligné à gauche, de taille approximative 23x23 points, pour l'icône « Lecture/Pause ». Le second sera le titre de la piste et devrait prendre le reste de la cellule. Cliquez sur votre label du bouton lecture puis sur Éditer → Emoji et Symboles dans la barre de menu Mac OS et trouvez une icône qui ressemble à un bouton lecture. J'en ai trouvé dans la section Emoji → Objets et Symboles. Pour défi, essayez d'utiliser une image pour l'icône du bouton !

1. import UIKit

2. class TrackCell: UITableViewCell {

3. @IBOutlet weak var playIcon: UILabel!

4. @IBOutlet weak var titreLabel: UILabel!

5. }

Quand vous aurez terminé, vous devriez avoir un prototype de cellule qui ressemble à ceci :

Dans le DetailsViewController, nous pouvons maintenant mettre en place les cellules personnalisées en prenant l'objet d'identifiant TrackCell et en faisant un cast vers notre propre classe avec « as TrackCell ».

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

2. let cell = tableView.dequeueReusableCellWithIdentifier("TrackCell") as TrackCell

4. let piste = pistes[indexPath.row]

5. cell.titleLabel.text = piste.titre

6. cell.playIcon.text = "VOTRE_ICONE_LECTURE"

8. return cell

9. }

La logique est presque la même que pour notre autre Table View, sauf que nous faisons un cast de la cellule vers notre classe TrackCell personnalisée, sur la première ligne. Le texte «VOTRE_ICONE_LECTURE » devrait être remplacé par l'icône de lecture que vous pouvez obtenir en cliquant sur Éditer → Emoji et Symboles dans la barre de menu Mac OS. N'oubliez pas les guillemets autour !

Ensuite, nous extrayons la piste dont nous avons besoin de la liste de pistes, comme auparavant avec les albums.

Finalement, nous prenons notre variable IBOutlet personnalisée, titreLabel, et assignons à son texte le titre de la piste, puis faisons de même pour playIcon.

Bravo d'avoir persévéré jusqu'ici, nous approchons du but !

V - Sonner la musique

Bien, pour la suite nous voulons faire en sorte de pouvoir écouter du son. Nous allons utiliser la classe MPMoviePlayerController pour cela. Elle est simple à utiliser et elle marche sans problème avec les flux audio sans vidéo.

Pour commencer, ajoutez le mediaPlayer comme propriété dans notre classe DetailsViewController. Directement en dessous de la définition de classe, insérez :

1. var mediaPlayer: MPMoviePlayerController = MPMoviePlayerController()

ERROR! Use of undeclared type MPMoviePlayerController.

C'est bon, l'erreur est juste que nous devons importer la bibliothèque MediaPlayer qui n'est pas incluse par défaut dans notre projet.

Ajoutez juste le code suivant en haut de votre fichier DetailsViewController :

1. import MediaPlayer

Ensuite, lançez la lecture de l'audio quand l'utilisateur sélectionne une des pistes. Ajoutez ceci à notre classe DetailsViewController :

1. func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

2. var piste = pistes[indexPath.row]

3. mediaPlayer.stop()

4. mediaPlayer.contentURL = NSURL(string: piste.previewUrl)

5. mediaPlayer.play()

6. if let cell = tableView.cellForRowAtIndexPath(indexPath) as? TrackCell {

7. cell.playIcon.text = "VOTRE_ICONE_STOP"

8. }

9. }

La ligne mediaPlayer.stop() interrompt la lecture de la piste. S'il n'y en a pas une en lecture à ce moment, rien ne se passe. Nous ne voulons pas jouer plusieurs pistes en simultané, alors assurons-nous d'interrompre la lecture d'une piste quand l'autre est cliquée.

Après, la propriété mediaPlayer.contentURL indique l'URL depuis laquelle le lecteur devrait charger son contenu. Dans notre cas, c'est celle qui est enregistrée dans piste.previewUrl.

Finalement, nous appelons mediaPlayer.play() et obtenons la cellule de la piste pour la ligne qui a été tapée. Si cette ligne est toujours visible, la variable cell sera initialisée et là nous pouvons changer le label playIcon pour remplacer l'icône lecture avec l'icône pause que nous pouvons trouver grâce à Éditer → Emoji et Symboles dans la barre de menu Mac OS.

Si vous lancez votre application, vous devriez maintenant avoir une application d'aperçu musique iTunes entièrement fonctionnelle ! En soi-même, c'est plutôt génial, mais mettons-y encore une chose pour la rendre encore plus cool, des animations bien léchées des cellules de la table.

VI - Ajouter des animations

C'est en fait très facile et donne un effet visuel très chouette.

Tout ce que nous allons faire c'est d'ajouter la fonction suivante à nos deux classes SearchResultsViewController et DetailsViewController :

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
cell.layer.transform = CATransform3DMakeScale(0.1,0.1,1)
UIView.animateWithDuration(0.25, animations: {
cell.layer.transform = CATransform3DMakeScale(1,1,1)
})
}

Démarrez l'application et faites-la défiler ; joli n'est-ce pas ?

Et comment ça marche ?

La fonction willDisplayCell est appelée depuis le délégué de TableView, de façon similaire à nos autres fonctions de callback qui préparent la ligne à afficher. Mais celle-ci n'est appelée que lorsqu'une cellule s'apprête à apparaître à l'écran, que ce soit pendant le chargement initial ou pendant le défilement de l'affichage.

1. cell.layer.transform = CATransform3DMakeScale(0.1,0.1,1)

Cette première ligne utilise CATransform3DMakeScale() pour créer une matrice de transformation qui réduit la taille de n'importe quel objet sur l'axe x, y et z. Si vous vous y connaissez en algèbre linéaire, vous saurez immédiatement ce que ça signifie. Sinon, ce n'est pas super important. Le point important est que ça redimensionne quelque chose, et ici nous le réduisons à 10 % de sa taille en donnant la valeur 0.1 à x et y.

Donc, en gros, tout ce que nous faisons est de rendre la transformation de la sous-couche de la cellule 90 % plus petite.

Après ça, nous mettons la transformation de la sous-couche de la cellule à une échelle différente, cette fois à (1,1,1). Cela veut juste dire qu'elle devrait revenir à l'échelle initiale. Puisque cette ligne fait partie du bloc de animateWithDuration(), nous avons une animation gratis de la part de Core Animation.

Les développeurs Objective-C chevronnés vont probablement noter que ce n'est pas la seule façon de faire une telle animation. Cependant, je crois que cette méthode est la plus facile à comprendre, en plus d'être la plus Swift.

Dans mon livre à venir, je reviens en grand détail sur comment utiliser Core Animation efficacement pour faire de belles animations pour vos applications. Utiliser Core Animation de cette manière fait vraiment ressortir votre app du lot.

VII - Code source

Le code source complet de cette section est disponible ici.

Un lecteur de ce tutoriel en ligne a contribué en offrant la prochaine section qui couvre la production d'un meilleur bouton de lecture/pause tout en code. Trouvez-la ici.

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

Rejoignez-nous sur nos forums.

IX - 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 bredelet pour sa traduction, LeBzul pour sa relecture technique ainsi que milkoseck pour sa relecture orthographique.

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