Dans le chapitre précédent, nous avons créé l'interface de notre application, mais malheureusement le sélecteur a échappé à notre folie artistique ! Nous n'avons pas pu le configurer dans le storyboard. Et du coup, quand vous lancez l'application, le résultat en la matière n'est pas probant...
Qu'à cela ne tienne, nous aurons sa peau dans le code !
Delegate, dataSource et MVC
En MVC, vous savez qu'il existe plusieurs formes de communication entre les trois parties. Ce schéma doit vous rappeler quelque chose :
Notamment, on y voit que la vue communique avec le contrôleur via les outlets et les actions. Nous allons découvrir ici deux nouvelles formes de communication : les dataSources et les delegates.
La dataSource
En MVC, une vue ne détient pas ses données. Car une vue n'a qu'un rôle d'affichage, ce n'est pas son rôle de savoir quoi afficher. C'est le contrôleur qui va le lui dire.
Ainsi, dans le cas de notre liste de races, ce n'est pas la vue qui doit retenir la liste de races, c'est le contrôleur qui va lui dire quoi afficher. Et il va tirer cette information du modèle qui détient les données.
Donc pour savoir quoi afficher, le contrôleur va simplement utiliser des outlets pour les vues relativement simples et surtout qui ont peu de données à afficher. Mais pour des vues plus complexes avec beaucoup de données à afficher, le contrôleur va se positionner en source de données pour que la vue puisse piocher dedans, et donc n'avoir en même temps que très peu de données à afficher.
Le contrôleur est donc la dataSource
, celle qui détient les données. Et voilà comment cela va fonctionner :
Le PickerView va nommer le contrôleur source de données.
Cela implique que le contrôleur s'engage à répondre à certaines questions précises du PickerView ; par exemple, combien d'éléments y a-t-il dans la liste ?
Lorsque la vue en a besoin, le contrôleur répond aux questions.
Vous venez de découvrir une nouvelle forme de communication aveugle entre le contrôleur et la vue.
Le delegate
Le delegate fonctionne de la même manière. Une vue peut avoir besoin de poser des questions à son contrôleur selon les évènements qui lui arrivent.
Mais parfois, on ne veut pas toujours faire la même action. Par exemple, lorsque l'utilisateur va faire défiler la liste des races, la vue va devoir demander à chaque fois : quel est le prochain nom de race que je dois afficher ? Et à chaque fois, le contrôleur va répondre une race différente. C'est lui qui choisit ce qu'on affiche.
Pour prendre un autre exemple, on peut parler des scroll view
. Une scroll view
va utiliser le mécanisme de delegate pour dire au contrôleur :
Je viens de scroller de quelques pixels, tu veux faire une action ?
Je viens d'atteindre la fin du scroll, on fait quelque chose ?
Je viens de m'arrêter de scroller, que veux-tu faire ?
Autrement dit, la vue délègue toute prise de décision au contrôleur. D'où le nom de ce mécanisme.
Le contrôleur devient ce qu'on appelle le delegate, celui à qui est déléguée la responsabilité. Et cela va fonctionner exactement comme pour la data source :
Le PickerView va nommer le contrôleur délégué.
Cela implique que le contrôleur s'engage à répondre à certaines questions précises du PickerView.
Lorsque la vue en a besoin, le contrôleur répond aux questions.
Encore une nouvelle forme de communication aveugle entre le contrôleur et la vue. Vos compétences en MVC ont fait un bond en avant !
Ces 3 étapes vont être les trois étapes que nous allons suivre pour configurer le delegate et la dataSource de notre PickerView.
Nommez la dataSource et le delegate
Lors de la première étape, la vue nomme le contrôleur sa dataSource et son delegate. Et nous allons faire cela avec... deux control-drags !
Il vous suffit de faire le control-drag depuis le Picker View vers le contrôleur, comme ceci :
Une pop-up apparaît. Cliquez sur dataSource
, puis répétez l'opération et cliquez cette fois sur delegate
.
Le contrôleur s'engage à répondre aux questions
Deuxième étape : le contrôleur s'engage à répondre aux questions de la vue. Pour cela, le contrôleur va implémenter ce qu'on appelle un protocole.
Un protocole, c'est un outil en Swift dont nous n'avons pas encore parlé. Un protocole est une liste de méthodes vides et de propriétés.
De méthodes vides ?
Oui, un protocole, c'est juste une liste de méthodes vides. Contrairement à une classe, dans laquelle on écrit une méthode et son implémentation, dans un protocole on n'a que la déclaration des fonctions.
Pour vous donner un exemple, voici le protocole UIPickerViewDataSource
.
protocol UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int
}
C'est juste une liste de méthodes.
À quoi ça sert ?
Justement, ça va nous être très utile ici. On veut que le contrôleur s'engage à répondre à des questions. Et les questions, ce sont simplement les méthodes du protocole !
Pour cela, notre contrôleur va se conformer au protocole. C'est un terme technique pour dire qu'il s'engage à fournir une implémentation aux méthodes qui se trouvent dans le protocole.
Pour concrétiser cet engagement, il suffit d'ajouter une extension à notre classe, et d’ajouter la conformance à ce protocole via l’extension, comme ceci :
extension FormViewController: UIPickerViewDataSource {
}
extension FormViewController: UIPickerViewDelegate {
}
Une fois que vous avez écrit cela, vous devriez voir des erreurs s'afficher.
Type 'FormViewController' does not conform to protocol 'UIPickerViewDataSource'
Le problème, c'est qu'on s'engage à implémenter des méthodes, mais on ne le fait pas ! Xcode n'est pas content !
Résolvons cela.
Le contrôleur répond aux questions
Il ne nous reste plus qu'à répondre aux questions. Et pour cela, nous allons implémenter les méthodes de nos deux protocoles : UIPickerViewDataSource
et UIPickerViewDelegate
.
Méthodes de la dataSource
La première, c'est donc :
func numberOfComponents(in pickerView: UIPickerView) -> Int {
}
Ajoutez cette fonction dans le code du FormViewController
. Nous allons l'implémenter. Cette méthode doit apparemment renvoyer un entier. Cet entier correspond au numberOfComponents de notre PickerView.
C'est quoi, les components du PickerView ?
Les components représentent les différentes colonnes de notre PickerView. Souvenez-vous, le PickerView fonctionne comme la roulette au casino. Par exemple, sur la roulette suivante, il y a quatre components.
Le nôtre est bien plus simple puisqu'on veut ajouter seulement une liste, celle des races de chiens.
Donc on va répondre simplement à la question en implémentant la méthode comme ceci :
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
Maintenant, la vue sait combien de components afficher.
Passons maintenant à la deuxième méthode :
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
}
Cette méthode retourne également un entier. Cette fois, il correspond au nombre d'éléments présents dans chaque colonne. C'est la raison pour laquelle nous avons component en paramètre, pour pouvoir adapter la réponse en fonction de la colonne.
Nous, nous n'avons qu'une seule colonne, donc on va simplement répondre par le nombre de races de chiens à afficher. Et pour cela, nous allons tout simplement utiliser le tableau dogRaces qui se trouve dans notre modèle.
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return dogRaces.count
}
Et voilà ! La vue connaît maintenant le nombre d'éléments à prévoir dans la liste.
Méthode du delegate
UIPickerViewDelegate
est un protocole qui contient bien plus de méthodes qui permettent de personnaliser complètement la vue. Mais il y en a une seule qui nous intéresse vraiment, c'est celle-ci, que je vous invite à rajouter dans votre code :
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
}
Cette méthode renvoie une string optionnelle qui correspond au titre que l'on veut mettre pour chaque élément. Évidemment, ce titre dépend de la colonne et de la ligne dont on parle. C'est la raison pour laquelle nous avons les entiers row
et component
en paramètres de cette méthode.
Encore une fois, le paramètre component
ne nous importe pas puisque nous n'avons qu'une seule colonne. En revanche, on va utiliser le paramètre row
en index de notre tableau dogRaces
.
Vous allez donc implémenter la méthode comme ceci :
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return dogRaces[row]
}
Et voilà ! Maintenant la vue sait quel titre afficher en fonction de la ligne à laquelle on se trouve ; vous pouvez lancer l'application et confirmer que tout fonctionne correctement !
Voici le code complet de nos deux extensions :
extension FormViewController: UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return dogRaces.count
}
}
extension FormViewController: UIPickerViewDelegate {
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return dogRaces[row]
}
}
En résumé
En MVC, une vue peut utiliser le mécanisme de la dataSource pour ne pas être propriétaire de ses données, et les réclamer au contrôleur.
En MVC, une vue peut utiliser le mécanisme du delegate pour déléguer les prises de décision au contrôleur.
Un protocole est une liste de méthodes vides et de propriétés.
Pour utiliser le mécanisme de dataSource ou delegate, on suit trois étapes :
La vue nomme le contrôleur sa dataSource et/ou delegate.
Le contrôleur s'engage à répondre aux questions de la vue en adoptant le protocole correspondant.
Le contrôleur répond aux questions de la vue en se conformant au(x) protocole(s), et donc en implémentant les méthodes de ce(s) dernier(s).
Dans le prochain chapitre, nous allons revenir sur nos champs de texte et apprendre à gérer le clavier !