Une UITableView triée par ordre alphabétique
L’UITableView est un outil idéal pour afficher une certaine quantité de données à l’utilisateur. Seulement voilà, quand cette quantité de données devient grande, il peut être intéressant de permettre à l’utilisateur d’accéder rapidement à l’information qu’il souhaite.
Une des solutions est d’utiliser le tri par ordre alphabétique. C’est l’idéal pour l’affichage de contacts (par exemple). On peut même aller plus loin. Pourquoi simplement se limiter à un tri par ordre alphabétique ? Pourquoi ne pas proposer une catégorisation des entrées en prenant en compte la première lettre de la propriété qui nous intéresse. C’est ce que nous allons voir dans ce billet. Au final, nous arriverons à ce résultat:
On commence par créer notre projet dans Xcode et on lui ajoute un TableViewController. Pour le moment, on n’y touche pas. Ensuite, on créée le xib des cellules avec Interface Builder (pour plus de détails sur la manipulation de UITableViewCell avec Interface Builder, voir Créer une UITableViewCell personnalisée à partir d’Interface Builder). Maintenant, faisons une structure pour nos données. Ce sera une simple classe contenant 4 propriétés NSString: firstname, lastname, age et sex. Ce qui donne tout simplement:
@interface Person : NSObject {
NSString *firstname;
NSString *lastname;
NSString *age;
NSString *sex;
}
On rajoute aussi les méthodes d’initialisation et de désallocation de l’objet:
- (id) initWithLastName:(NSString *)mlastname andFirstname:(NSString *)mfirstname andSex:(NSString *)msex andAge:(NSString *)mage {
lastname = [[NSString alloc] initWithString:mlastname];
firstname = [[NSString alloc] initWithString:mfirstname];
sex = [[NSString alloc] initWithString:msex];
age = [[NSString alloc] initWithString:mage];
return [super init];
}
- (void) dealloc {
[lastname release];
[firstname release];
[sex release];
[age release];
[super dealloc];
}
Toute la partie manipulation de données sera gérée par PersonDataSource. Il s’agit d’une classe contenant:
- un
NSArray(personList) qui contiendra notre liste dePersonbrute (c’est à dire non-triée) - un
NSMutableDictionary(personListAlphabeticalDictionary) qui contiendra nosPersontriées par ordre alphabétique. Chaque entrée du dictionnaire aura pour clef la première lettre du nom desPersonet en valeur, le tableau dePersoncorrespondant. - un
NSArray(personNameFirstLeterIndexArray) qui contiendra la liste de toutes les premières lettres de noms disponibles. En effet, il se peut que l’on n’ai aucune entrée avec comme première lettre Z (par exemple).
Du coté des méthodes, ça donne cela:
- (void) populate: cette méthode ne fait que récupérer nos objetsPerson. Dans l’exemple, j’ajoute les objets à la main, mais on peut tout à fait remplacer ça par une récupération de données dans un fichier XML ou bien une base de données.- (void) populatePersonListAlphabeticalDictionary: cette méthode va aller chercher le contenu depersonListet générerpersonListAlphabeticalDictionarycomme convenu. Pour finir, la méthode va appelersortPersonInitialLetterIndexes.- (void) sortPersonInitialLetterIndexes: cette méthode va générerpersonNameIndexArrayen récupérant toutes les clefs depersonListAlphabeticalDictionary(triées alphabétiquement). Puis pour chacune des clefs,presortPersonListAlphabeticalDictionaryForInitialLetter- (void)presortPersonListAlphabeticalDictionaryForInitialLetter:(NSString *)aKeyva (pour une clefaKeydonnée), trier le contenu duNSArraycorrespondant dans lepersonListAlphabeticalDictionary.- (NSArray *)personsWithInitialLetter:(NSString*)aKey: cette méthode retourne unNSArrayavec toutes lesPersonayant pour première lettreaKey.- (Person *) personForIndexPath:(NSIndexPath *)indexPathretourne l’objetPersoncorrespondant auNSIndexPathpassé en paramètre. Cette méthode sera appelée par leUITableViewController.
Au final, on obtient ceci:
- (id) init {
[self populate];
[self populatePersonListAlphabeticalDictionary];
return [super init];
}
- (void) dealloc {
[personList release];
[personListAlphabeticalDictionary release];
[personNameIndexArray release];
[super dealloc];
}
- (void) populate {
personList = [[NSMutableArray alloc] init];
[personList addObject:[[Person alloc] initWithLastName:@"Léponge" andFirstname:@"Bob" andSex:@"Homme" andAge:@"21"]];
[personList addObject:[[Person alloc] initWithLastName:@"Daniel" andFirstname:@"Jack" andSex:@"Whiskey" andAge:@"12"]];
[personList addObject:[[Person alloc] initWithLastName:@"Fontaine" andFirstname:@"Claire" andSex:@"Femme" andAge:@"89"]];
[personList addObject:[[Person alloc] initWithLastName:@"Charues" andFirstname:@"Vielles" andSex:@"Festival" andAge:@"125"]];
[personList addObject:[[Person alloc] initWithLastName:@"Bull" andFirstname:@"Red" andSex:@"Boisson" andAge:@"2"]];
[personList addObject:[[Person alloc] initWithLastName:@"Bonvoisin" andFirstname:@"Bernie" andSex:@"Homme" andAge:@"55"]];
[personList addObject:[[Person alloc] initWithLastName:@"Jobs" andFirstname:@"Steeve" andSex:@"Homme" andAge:@"80"]];
[personList addObject:[[Person alloc] initWithLastName:@"Damien" andFirstname:@"Francois" andSex:@"Humoriste" andAge:@"35"]];
[personList addObject:[[Person alloc] initWithLastName:@"Gates" andFirstname:@"Bill" andSex:@"Homme" andAge:@"50"]];
[personList addObject:[[Person alloc] initWithLastName:@"Laporte" andFirstname:@"Bernard" andSex:@"Rugbyman" andAge:@"32"]];
[personList addObject:[[Person alloc] initWithLastName:@"Chabal" andFirstname:@"Sebastien" andSex:@"Mythe" andAge:@"36"]];
}
- (void) populatePersonListAlphabeticalDictionary {
personListAlphabeticalDictionary = [[NSMutableDictionary alloc] init];
for(Person *currentPerson in personList)
{
NSString *firstLetter = [currentPerson.lastname substringToIndex:1];
NSMutableArray *existingArray;
/* Si on a déjà un array de Person pour cette première lettre*/
if (existingArray = [personListAlphabeticalDictionary valueForKey:firstLetter])
{
/* On y ajoute la Person en cours*/
[existingArray addObject:currentPerson];
}
else /*Sinon, on créée l'array, on y ajoute la Person courante et on ajoute l'array au dictionnaire*/
{
NSMutableArray *tempArray = [NSMutableArray array];
[tempArray addObject:currentPerson];
[personListAlphabeticalDictionary setObject:tempArray forKey:firstLetter];
}
}
/* On génère personNameIndexArray et on trie chaque array de personListAlphabeticalDictionary*/
[self sortPersonInitialLetterIndexes];
}
- (void) sortPersonInitialLetterIndexes {
personNameIndexArray = [[NSMutableArray alloc] init];
/* On récupère toutes les premières lettres et on les trie en ignorant la case*/
personNameIndexArray = [[personListAlphabeticalDictionary allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
for (NSString * eachNameIndex in personNameIndexArray)
{
[self presortPersonListAlphabeticalDictionaryForInitialLetter:eachNameIndex];
}
}
- (void)presortPersonListAlphabeticalDictionaryForInitialLetter:(NSString *)aKey {
/* Pour chaque array à la clef aKey dans le dictionnaire, on trie le contenu de l'array alphabétiquement sur la propriété lastname en ignorant la case*/
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastname"
ascending:YES
selector:@selector(localizedCaseInsensitiveCompare:)];
NSArray *descriptors = [NSArray arrayWithObject:nameDescriptor];
[[personListAlphabeticalDictionary objectForKey:aKey] sortUsingDescriptors:descriptors];
[nameDescriptor release];
}
- (Person *) personForIndexPath:(NSIndexPath *)indexPath {
/* On récupère la lettre qui a un ID correspondant à la section de l'indexPath */
NSArray *personsWithInitLetter = [self personsWithInitialLetter:[personNameIndexArray objectAtIndex:indexPath.section]];
/* Et on renvoie la Person correspondant au row*/
return [personsWithInitLetter objectAtIndex:indexPath.row];
}
- (NSArray *)personsWithInitialLetter:(NSString*)aKey {
/* On renvoie la liste des Person ayant pour première lettre aKey*/
return [personListAlphabeticalDictionary objectForKey:aKey];
}
Maintenant que notre source de données est prête, il n’y a plus qu’à mettre en place la TableView en elle-même. La première chose est d’ajouter un personDataSource comme variable d’instance et de l’initialiser (via [[PersonDataSource alloc]init];). Ensuite, on attaque le . Le fonctionement est traditionnel. La seule subtilité, c’est qu’on va récupérer l’objet
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPathPerson via [personDataSource personForIndexPath:indexPath].
Pour le nombre de sections dans la table, c’est simple: il y en aura autant que de première lettres. Donc, c’est tout naturellement que - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView renvera [[personDataSource personNameIndexArray] count]. Pour le nombre de lignes dans chaque section, c'est le nombre de Person ayant pour première lettre celle qui correspond à la section courante. C'est pourquoi retourne
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)sectionreturn [[self miniContactsWithInitialLetter:[miniContactNameIndexArray objectAtIndex:section]] count];. On obtient donc ce code pour le UITableViewController:
@implementation AlphaTableViewController
- (id)initWithStyle:(UITableViewStyle)style{
if (self = [super initWithStyle:style])
{
personDataSource = [[PersonDataSource alloc]init];
self.tableView.rowHeight = 125;
}
return self;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"MyCellIdent";
TableCell *cell = (TableCell *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
UIViewController *c = [[UIViewController alloc] initWithNibName:@"TableViewCell" bundle:nil];
cell = (TableCell *)c.view;
[c release];
}
Person *currentPerson = [personDataSource personForIndexPath:indexPath];
[cell.firstname setText:[currentPerson firstname]];
[cell.lastname setText:[currentPerson lastname]];
[cell.sex setText:[currentPerson sex]];
[cell.age setText:[currentPerson age]];
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [[personDataSource personNameIndexArray] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[personDataSource personsWithInitialLetter:[[personDataSource personNameIndexArray] objectAtIndex:section]] count];
}
Cela donne le résultat ci-dessous. C'est bien, mais il manque encore la liste des lettres sur la droite. C'est pas compliqué
.

En fait, il faut être capable de répondre à deux questions posées à l'UITableViewController: quels sont les titres à afficher pour l'index de la TableView via et Pour la section X, où faut-il scroller la TableView via
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index. Au final, il est très facile de répondre à ces questions dans l'architecture que l'on a mis en place. Voilà ce que ça donne dans le code:
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
return [personDataSource personNameIndexArray];
}
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
return index;
}
Et le résultat final ... Vous l'avez déjà vu plus haut. Si vous voulez, vous trouverez ici une archive du projet complet (Xcode 3.1).


Mistunk a écrit
le 18 mars 2010 à 23:58
Le rendu ne semble pas ok, je suis sous xcode 3.2 avec un minimum d’iphone OS 3.0 pour compiler, y a t’il des differences à prendre en compte avec cette version ?
Julien Quéré a écrit
le 21 mars 2010 à 19:04
Bonsoir,
Malheureusement, cet article est largement outdated. J’avoue ne pas avoir fait de cellules sous IB depuis 3.0. Soit je les fait à la main, soit j’utilise Three20.
Désolé.