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) :
Cliquez pour lire la vidéo
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 :
2.
3.
func
get
(
path
:
String
)
{
let
url
=
NSURL
(
string
:
path
)
...
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
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 :
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.
import
Foundation
protocol
APIControllerProtocol
{
func
didReceiveAPIResults
(
results
:
NSArray
)
}
class
APIController
{
var
delegate
:
APIControllerProtocol
init
(
delegate
:
APIControllerProtocol
)
{
self
.
delegate
=
delegate
}
func
get
(
path
:
String
)
{
let
url
=
NSURL
(
string
:
path
)
let
session
=
NSURLSession
.
sharedSession
()
let
task
=
session
.
dataTaskWithURL
(
url
!,
completionHandler
:
{
data
,
response
,
error
->
Void
in
println
(
"Tâche terminée"
)
if
(
error
!=
nil
)
{
// S'il y a une erreur dans la requête Web, l'afficher dans la console
println
(
error
.
localizedDescription
)
}
var
err
:
NSError
?
if
let
jsonResult
=
NSJSONSerialization
.
JSONObjectWithData
(
data
,
options
:
NSJSONReadingOptions
.
MutableContainers
,
error
:
&
err
)
as
?
NSDictionary
{
if
(
err
!=
nil
)
{
// S'il y a une erreur dans l'interprétation du JSON, l'afficher dans la console
println
(
"Erreur JSON
\(
err
!.
localizedDescription
)
"
)
}
if
let
results
:
NSArray
=
jsonResult
[
"results"
]
as
?
NSArray
{
self
.
delegate
.
didReceiveAPIResults
(
results
)
}
}
})
// task est juste un objet avec toutes ces propriétés définies
// Afin d'exécuter réellement la requête Web, nous devons appeler resume()
task
.
resume
()
}
func
searchItunesFor
(
searchTerm
:
String
)
{
// L'API iTunes demande des termes multiples, séparés par des + , alors remplacez les espaces par des +
let
itunesSearchTerm
=
searchTerm
.
stringByReplacingOccurrencesOfString
(
" "
,
withString
:
"+"
,
options
:
NSStringCompareOptions
.
CaseInsensitiveSearch
,
range
:
nil
)
// Maintenant, échappez tout ce qui n'est pas URL-friendly
if
let
escapedSearchTerm
=
itunesSearchTerm
.
stringByAddingPercentEscapesUsingEncoding
(
NSUTF8StringEncoding
)
{
let
urlPath
=
"https://itunes.apple.com/search?term=
\(
escapedSearchTerm
)
&media=music&entity=album"
}
}
}
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 à :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
func
searchItunesFor
(
searchTerm
:
String
)
{
// L'API iTunes demande des termes multiples, séparés par des + , alors remplacez les espaces par des +
let
itunesSearchTerm
=
searchTerm
.
stringByReplacingOccurrencesOfString
(
" "
,
withString
:
"+"
,
options
:
NSStringCompareOptions
.
CaseInsensitiveSearch
,
range
:
nil
)
// Maintenant, échappez tout ce qui n'est pas URL-friendly
if
let
escapedSearchTerm
=
itunesSearchTerm
.
stringByAddingPercentEscapesUsingEncoding
(
NSUTF8StringEncoding
)
{
let
urlPath
=
"https://itunes.apple.com/search?term=
\(
escapedSearchTerm
)
&media=music&entity=album"
get
(
urlPath
)
}
}
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
.
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
if
let
collectionId
=
result
[
"collectionId"
]
as
?
Int
{
var
newAlbum
=
Album
(
nom
:
nom
!,
prix
:
prix
!,
miniatureImageURL
:
miniatureURL
!,
largeImageURL
:
imageURL
!,
itemURL
:
itemURL
!,
artisteURL
:
artisteURL
!,
collectionId
:
collectionId
!)
albums
.
append
(
newAlbum
)
}
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
.
@IBOutlet
weak
var
pistesTableView
:
UITableView
!
Puis, choisissons le DetailsViewController comme dataSource et délégué du TableView, et implémentons le protocole comme auparavant :
2.
3.
4.
5.
6.
7.
func
tableView
(
tableView
:
UITableView
,
numberOfRowsInSection section
:
Int
)
->
Int
{
return
0
}
func
tableView
(
tableView
:
UITableView
,
cellForRowAtIndexPath indexPath
:
NSIndexPath
)
->
UITableViewCell
{
return
UITableViewCell
()
}
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
.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
import
Foundation
struct
Track
{
let
titre
:
String
let
prix
:
String
let
previewUrl
:
String
init
(
titre
:
String
,
prix
:
String
,
previewUrl
:
String
)
{
self
.
titre
=
titre
self
.
prix
=
prix
self
.
previewUrl
=
previewUrl
}
}
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é.
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.
2.
3.
func
lookupAlbum
(
collectionId
:
Int
)
{
get
(
"https://itunes.apple.com/lookup?id=
\(
collectionId
)
&entity=song"
)
}
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.
2.
3.
class
DetailsViewController
:
UIViewController
,
APIControllerProtocol
{
lazy
var
api
:
APIController
=
APIController
(
delegate
:
self
)
...
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 :
2.
3.
4.
// Charger les pistes
if
self
.
album
!=
nil
{
api
.
lookupAlbum
(
self
.
album
!.
collectionId
)
}
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.
2.
3.
4.
5.
6.
7.
8.
// MARQUE: APIControllerProtocol
func
didReceiveAPIResults
(
results
:
NSArray
)
{
dispatch_async
(
dispatch_get_main_queue
(),
{
self
.
pistes
=
Track
.
tracksWithJSON
(
results
)
self
.
pistesTableView
.
reloadData
()
UIApplication
.
sharedApplication
().
networkActivityIndicatorVisible
=
false
})
}
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.
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.
static
func
tracksWithJSON
(
results
:
NSArray
)
->
[
Track
]
{
var
pistes
=
[
Track
]()
for
pisteInfo
in
results
{
// Créer la piste
if
let
type
=
pisteInfo
[
"kind"
]
as
?
String
{
if
type
==
"song"
{
var
prix
=
pisteInfo
[
"trackPrice"
]
as
?
String
var
titre
=
pisteInfo
[
"trackName"
]
as
?
String
var
previewUrl
=
pisteInfo
[
"previewUrl"
]
as
?
String
if
(
titre
==
nil
)
{
titre
=
"Inconnu"
}
else
if
(
prix
==
nil
)
{
println
(
"Aucun prix disponible en
\(
pisteInfo
)
"
)
prix
=
"?"
}
else
if
(
previewUrl
==
nil
)
{
previewUrl
=
""
}
var
piste
=
Track
(
titre
:
titre
!,
prix
:
prix
!,
previewUrl
:
previewUrl
!)
pistes
.
append
(
piste
)
}
}
}
return
pistes
}
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 :
2.
3.
func
tableView
(
tableView
:
UITableView
,
numberOfRowsInSection section
:
Int
)
->
Int
{
return
tracks
.
count
}
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 !
2.
3.
4.
5.
import
UIKit
class
TrackCell
:
UITableViewCell
{
@IBOutlet
weak
var
playIcon
:
UILabel
!
@IBOutlet
weak
var
titreLabel
:
UILabel
!
}
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 ».
2.
3.
4.
5.
6.
7.
8.
9.
func
tableView
(
tableView
:
UITableView
,
cellForRowAtIndexPath indexPath
:
NSIndexPath
)
->
UITableViewCell
{
let
cell
=
tableView
.
dequeueReusableCellWithIdentifier
(
"TrackCell"
)
as
TrackCell
let
piste
=
pistes
[
indexPath
.
row
]
cell
.
titleLabel
.
text
=
piste
.
titre
cell
.
playIcon
.
text
=
"VOTRE_ICONE_LECTURE"
return
cell
}
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 :
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 :
import
MediaPlayer
Ensuite, lançez la lecture de l'audio quand l'utilisateur sélectionne une des pistes. Ajoutez ceci à notre classe DetailsViewController :
2.
3.
4.
5.
6.
7.
8.
9.
func
tableView
(
tableView
:
UITableView
,
didSelectRowAtIndexPath indexPath
:
NSIndexPath
)
{
var
piste
=
pistes
[
indexPath
.
row
]
mediaPlayer
.
stop
()
mediaPlayer
.
contentURL
=
NSURL
(
string
:
piste
.
previewUrl
)
mediaPlayer
.
play
()
if
let
cell
=
tableView
.
cellForRowAtIndexPath
(
indexPath
)
as
?
TrackCell
{
cell
.
playIcon
.
text
=
"VOTRE_ICONE_STOP"
}
}
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.
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 »