TIMFlix – Délégation et UITableView personnalisée

Description:
Avec ce tutoriel, nous apprendrons à construire une application qui affiche, grace à ‘UITableView’ et une cellule personnalisée, une liste de titres vidéos renseignée par un fichier de propriétés (plist).
Une page contenant le détail de la vidéo sera affichée – par un ‘segue’ – suite à la sélection d’un élément de la table.
Voici le ‘storyboard’:

Figure 1


Ainsi qu’une vidéo du résultat final:

Objectifs:
Savoir créer un tableau de type ‘NSArray’ à partir d’un fichier de propriétés.  Comprendre et utiliser la délégation de l’objet ‘UITableView’.  Etre capable de préparer et lancer par programmation, une transition vers une autre scène (‘segue’).  Savoir programmer une ‘UITableViewCell’ personnalisée (design et classe).
Éléments de contenu:

Le projet de départ est ici
 


Étape 1

À cette étape, nous allons mettre en place les scènes du projet, programmer une transition (segue), de la page de chargement vers la liste, positionner un UITableView puis finalement, renseigner une transition de la liste vers la scène de détail.
ACTION – Ouvrons le ‘Storyboard’ du projet de départ:

figure 2


 
ACTION – Ajoutons deux nouvelles scènes avec leur classe: VCListeDesVideos et VCDetailVideoCourante:
 
timflix-figure02

figure 3

Vidéo étape01


ACTION – Ajoutons de la couleur, un UITableView ainsi qu’un déplacement (segue) à partir la liste des vidéos vers la scène de détail:

timflix-figure03

figure 4

 Vidéo étape01.1


La programmation de la transition, de la scène d’introduction vers la scène  de la liste des vidéos, est réalisée avec les méthodes ‘performSelector: afterDelay:’ et ‘performSegueWithIdentifier’.
Voici le code à ajouter à la classe de la scène d’introduction:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self performSelector:@selector(transitionVersListeVideos) withObject:self afterDelay:3];
}
-(void) transitionVersListeVideos {
    [self performSegueWithIdentifier:@"versListeVideos" sender:self];
}

L’étape 1 complétée est ici


Étape 2

Objectifs:

Étape 2.1
Commençons par créer un tableau ‘NSArray‘ à partir de la liste de propriétés ‘Listes des videos.plist’.  Note, ce fichier est dans le projet de départ.
Voici un extrait du fichier contenant les données des vidéos:

<array>
	<dict>
		<key>titre</key>
		<string>Pirates du cyberspace</string>
		<key>duree</key>
		<string>1 h 48 m</string>
		<key>classement</key>
		<string>18A</string>
		<key>pochette</key>
		<string>pochette-000.jpg</string>
		<key>annee</key>
		<string>1995</string>
		<key>description</key>
		<string>L&apos;utilisation des ordinateurs lui étant bannie pour des années, un ex-enfant prodige replonge dans le monde du crime informatique avec trois autres pirates.</string>
		<key>cote</key>
		<string>4</string>
	</dict>
	<dict>
		<key>titre</key>
		<string>Star trek: The Next Generation </string>
		<key>duree</key>
		<string>1 h 48 m</string>
		<key>classement</key>
		<string>18A</string>
		<key>pochette</key>
		<string>pochette-007.jpg</string>
		<key>annee</key>
		<string>1987</string>
		<key>description</key>
		<string>In the year 2364, Capt. Jean-Luc Picard leads the new Enterprise on missions of discovery. First Officer William Riker, engineer Geordi La Forge, and Klingon crewmember Worf join Picard as they explore the universe and interact with alien species.</string>
		<key>cote</key>
		<string>4</string>
	</dict>
...
<array>

Ce fichier va nous permettre de créer un tableau qui contiendra des objets de type ‘NSDictionary’
Nous pourrons accéder aux éléments du tableau avec la syntaxe:
tableauDesVideos[noElement]
et
tableauDesVideos[noElement][@ »pochette »], …
ACTION – Modifions la classe de la scène ‘Liste des vidéos’.
Note:  nous voulons créer le tableau des vidéos au chargement de la scène ‘Liste des vidéos’, c’est pourquoi nous modifions le fichier ‘VCListeDesVideos.m’ et non pas le fichier ‘ViewController.m’.
 

@interface VCListeDesVideos (){
// Variables de classe
     NSArray * tableauDesVideos;
}
@end
@implementation VCListeDesVideos
- (void)viewDidLoad
{
    [super viewDidLoad];
    tableauDesVideos = [[NSArray alloc]initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"Listes des videos" ofType:@"plist"]];
    // NSLog(@"tableauDesVideos = %@", tableauDesVideos);
    NSLog(@"Pochette de la première vidéo = %@", tableauDesVideos[0][@"pochette"]);
}
@end

 
Note: Nous aurions pu créer un objet de type NSDictionary à partir d’un des éléments du ‘tableauDesVideos’ en procédant comme suit:

    tableauDesVideos = [[NSArray alloc]initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"Listes des videos" ofType:@"plist"]];
    NSDictionary * uneVideo = tableauDesVideos[0];
    NSLog(@"Titre = %@, durée = %@", uneVideo[@"titre"], uneVideo[@"duree"]);

 
 

Vidéo étape02.1

Étape 2.2 – Renseigner et programmer la délégation de l’objet UITableView

Pour être en mesure d’utiliser un objet de type UITableView dans un projet, il faudra se déclarer délégué d’au moins le protocole ‘UITableViewDataSource’ et au besoin ‘UITableViewDataSource’.
Etre délégué pour un objet, c’est devoir implémenter des méthodes dont les signatures sont prescrites par la super classe.
Par exemple, pour être en mesure de populer notre UITableView  à partir du tableau des vidéos, une de nos classes devra souscrire au protocole ‘UITableViewDataSource’ et programmer les deux méthodes obligatoires suivante:

– (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:
– (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:

Le fichier.h d’une classe  nous renseigne sur les méthodes de délégation, le niveau de prescription (@required ou @optional) ainsi que sur la syntaxe.
 
Voici un extrait du fichier UITableView.h

@protocol UITableViewDataSource<NSObject>
@required
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
// Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier:
// Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
@optional
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;              // Default is 1 if not implemented
...
@protocol UITableViewDelegate<NSObject, UIScrollViewDelegate>
@optional
...
// Called after the user changes the selection.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
...

 
Une source d’information sur les protocoles et méthodes de délégation d’une classe est la documentation developpeur Apple.
Par exemple, pour UITableView:
https://developer.apple.com/library/ios/documentation/uikit/reference/UITableViewDataSource_Protocol/Reference/Reference.html
https://developer.apple.com/library/ios/documentation/uikit/reference/UITableViewDelegate_Protocol/Reference/Reference.html
Passons à l’action.  Dans la prochaine vidéo, nous allons localiser le fichier entête de la classe UITableView pour indentifier et implémeter les méthodes déléguées qui vont nous permettre de renseigner les données à afficher via le UITableView de la scène ‘Liste des vidéos’.
 

 
Action – Ajoutons le code suivant au fichier ‘VCListeDesVideos.m’:

// Les méthodes de délégation de données d'un UITableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return tableauDesVideos.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    // le paramètre 'indexPath.row' renseigne sur la cellule courante.
    // Il peut-être utilisé pour indicer le tableau des vidéos.
    UITableViewCell * celluleCourante = [[UITableViewCell alloc]init];
    celluleCourante.textLabel.text = tableauDesVideos[indexPath.row][@"titre"];
    // Ajoutons une image par programmation ...
    celluleCourante.imageView.image = [UIImage imageNamed:tableauDesVideos[indexPath.row][@"pochette"]];
    return celluleCourante;
}

 
Action – Traçons un lien entre la propriété ‘dataSource’ de notre objet ‘UITableView’ et le ‘ViewController’ de la scène ‘Liste des vidéos’
 

timflix-figure04

figure 5


 
Si nous testons l’application nous allons constater que le bouton, permettant de passer à la scène de détail n’est pas affiché.  Il n’est donc pas possible de lancer la transition suite à la sélection d’un item de la liste.
Cela s’explique par le fait que nous n’avons pas utilisé le modèle de la cellule personnalisée pour dessiner les cellules.  Dans ce cas, le UITableView utilise le modèle par défaut de UITableViewCell.  Ce modèle possède des propriétés visuelles comme celles que nous avons utilisées précédemment; celluleCourante.imageView, celluleCourante.textLabel …
Pour utiliser le modèle personnalisé de notre UITableViewCell il faut:

  1. Donner un identificateur à la cellule.
  2. Créer la cellule avec la méthode ‘dequeueReusableCellWithIdentifier’ du paramètre ‘tableView’

 
Action – Renseignons l’identificateur de UITableViewCell

timflix-figure06

figure 6


 
Action – Modifions la méthode ‘cellForRowAtIndexPath: indexPath:’ du fichier ‘VCListeDesVideos.m’
 

// tableView: cellForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell * celluleCourante = [tableView dequeueReusableCellWithIdentifier:@"modeleCellule" forIndexPath:indexPath];
    celluleCourante.textLabel.text = tableauDesVideos[indexPath.row][@"titre"];
    celluleCourante.imageView.image = [UIImage imageNamed:tableauDesVideos[indexPath.row][@"pochette"]];
    return celluleCourante;
}

Action – Il ne reste maintenant plus qu’à définir un lien IBAction sur le bouton de la scène détail (méthode: revenirScenePrecedente) et y ajouter le code suivant:

//  VCDetailVideoCourante.m
- (IBAction)revenirScenePrecedente:(id)sender {
    [self dismissViewControllerAnimated:YES completion:nil];
}

 Résultat final de l’étape 2


L’étape 2 complétée est ici


 

Étape 3 – Personnaliser la cellule du UITableView

Objectifs:

 
Étape 3.1
Action – Réalisons le design de la cellule

timflix-figure06.5

figure 7


Testons
timflix-figure07

figure 8


Note: Les objets visuels du modèle de cellule par défaut entrent en conflits avec nos éléments de design.  Il n’y a que la cellule sélectionnée qui est affichée correctement.
Action – Pour l’instant, plaçons la ligne suivante en commentaire:

//  VCListeDesVideos.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell * celluleCourante = [tableView dequeueReusableCellWithIdentifier:@"modeleCellule" forIndexPath:indexPath];
    // ---- > celluleCourante.textLabel.text = tableauDesVideos[indexPath.row][@"titre"];
    celluleCourante.imageView.image = [UIImage imageNamed:tableauDesVideos[indexPath.row][@"pochette"]];
    return celluleCourante;
}

Ce qui produit ceci à l’exécution de l’application:

timflix-figure08

figure 9


Étape 3.2
Action – Réalisons le design de la scène ‘Détail’
 
timflix-figure09

figure 10


 

Étape 3.3 – Classe personnalisée pour notre ‘UITableViewCell’

 
Nous allons ajouter une nouvelle classe à notre projet.  Cette classe sera dérivée de la classe UITableViewCell.  Elle deviendra la classe de notre cellule personnalisée.  Cela nous permettra de définir des liens MVC et de renseigner correctement les éléments visuels à l’intérieur de la méthode ‘cellForRowAtIndexPath: indexPath: ».
 
Action – Réalisons l’étape 3.3  (la recette est dans la vidéo suivante)
 

 
Voici les liens MVC de la cellule:
 

//  CelluleVideo.h
//  projet-TIMFlix-etape03
@property (weak, nonatomic) IBOutlet UIImageView *videoImage;
@property (weak, nonatomic) IBOutlet UILabel *videoTitre;
@property (weak, nonatomic) IBOutlet UILabel *videoAnnee;
@property (weak, nonatomic) IBOutlet UIImageView *videoCote;

 
Et les modifications au code du fichier ‘VCListeDesVideos.m’
 

#import "CelluleVideo.h"
// tableView: cellForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    CelluleVideo * celluleCourante = [tableView dequeueReusableCellWithIdentifier:@"modeleCellule" forIndexPath:indexPath];
    celluleCourante.videoTitre.text = tableauDesVideos[indexPath.row][@"titre"];
    celluleCourante.videoImage.image = [UIImage imageNamed:tableauDesVideos[indexPath.row][@"pochette"]];
    celluleCourante.videoAnnee.text = tableauDesVideos[indexPath.row][@"annee"];
    celluleCourante.videoCote.image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-star-rating.png", tableauDesVideos[indexPath.row][@"cote"]]];
    return celluleCourante;
}

 
L’étape 3 complétée est ici


 

Étape 4

Objectifs:

 
Contenu du fichier VCDetailVideoCourante.h

//  VCDetailVideoCourante.h
#import <UIKit/UIKit.h>
@interface VCDetailVideoCourante : UIViewController
@property NSDictionary *videoInfo;
@property (weak, nonatomic) IBOutlet UILabel *titre;
@property (weak, nonatomic) IBOutlet UIImageView *pochette;
@property (weak, nonatomic) IBOutlet UILabel *annee;
@property (weak, nonatomic) IBOutlet UILabel *duree;
@property (weak, nonatomic) IBOutlet UILabel *classement;
@property (weak, nonatomic) IBOutlet UIImageView *cote;
@property (weak, nonatomic) IBOutlet UITextView *description;
- (IBAction)revenirScenePrecedente:(id)sender;
@end

 
Contenu du fichier VCDetailVideoCourante.m
 

//  VCDetailVideoCourante.m
#import "VCDetailVideoCourante.h"
@interface VCDetailVideoCourante ()
@end
@implementation VCDetailVideoCourante
- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"detail = %@", self.videoInfo);
    self.titre.text = self.videoInfo[@"titre"];
    self.pochette.image = [UIImage imageNamed:self.videoInfo[@"pochette"]];
    self.annee.text = self.videoInfo[@"annee"];
    self.cote.image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-star-rating.png", self.videoInfo[@"cote"]]];
    self.duree.text = self.videoInfo[@"duree"];
    self.classement.text = self.videoInfo[@"classement"];
    self.description.text = self.videoInfo[@"description"];
}
- (IBAction)revenirScenePrecedente:(id)sender {
    [self dismissViewControllerAnimated:YES completion:nil];
}
@end

 
Code à ajouter au fichier VCListeDesVideos.m

//  VCListeDesVideos.m
#import "VCDetailVideoCourante.h"
// Lancée automatiquement avant une transition 'segue'
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    // Pointer sur la scène de destination
    VCDetailVideoCourante * vcDetail = [segue destinationViewController];
    // sender -> pointe sur la cellule sélectionnée
    int selectionCourante = [[self.TViewVideos indexPathForCell:sender] row];
    // Envoyer les informations de la sélection courante
    vcDetail.videoInfo = tableauDesVideos[selectionCourante];
}

 
Action – tester l’application
 
Améliorations à apporter:

 
L’étape 4 complétée est ici


 
 

Didacticiel préparé par Alain Boudreault, enseignant en Techniques d’intégration multimédia du cégep de Saint-Jérôme.  Version 1.0