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:

alphabeticaltableviewfinalscreen

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 de Person brute (c’est à dire non-triée)
  • un NSMutableDictionary (personListAlphabeticalDictionary) qui contiendra nos Person triées par ordre alphabétique. Chaque entrée du dictionnaire aura pour clef la première lettre du nom des Person et en valeur, le tableau de Person correspondant.
  • 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 objets Person. 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 de personList et générer personListAlphabeticalDictionary comme convenu. Pour finir, la méthode va appeler sortPersonInitialLetterIndexes.
  • - (void) sortPersonInitialLetterIndexes: cette méthode va générer personNameIndexArray en récupérant toutes les clefs de personListAlphabeticalDictionary (triées alphabétiquement). Puis pour chacune des clefs, presortPersonListAlphabeticalDictionaryForInitialLetter
  • - (void)presortPersonListAlphabeticalDictionaryForInitialLetter:(NSString *)aKey va (pour une clef aKey donnée), trier le contenu du NSArray correspondant dans le personListAlphabeticalDictionary.
  • - (NSArray *)personsWithInitialLetter:(NSString*)aKey: cette méthode retourne un NSArray avec toutes les Person ayant pour première lettre aKey.
  • - (Person *) personForIndexPath:(NSIndexPath *)indexPath retourne l’objet Person correspondant au NSIndexPath passé en paramètre. Cette méthode sera appelée par le UITableViewController.

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
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
. Le fonctionement est traditionnel. La seule subtilité, c’est qu’on va récupérer l’objet Person 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
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
retourne return [[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é :) .
alphabeticaltableviewmiddlescreen

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
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
et Pour la section X, où faut-il scroller la TableView via
- (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).

Cette entrée a été publiée dans Pro, avec comme mot(s)-clef(s) , , , . Vous pouvez la mettre en favoris avec ce permalien.

2 réponses à Une UITableView triée par ordre alphabétique

  1. Mistunk dit :

    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 ?

  2. 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é.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>