L'utilisation de la classe UITableViewController est fondamentale et très fréquente dans le développement d'applications iOS. Ce contrôleur de vue spécialisé simplifie considérablement la gestion des tables en fournissant une infrastructure préconfigurée pour afficher des données sous forme de liste.
Intégration de UITableViewController
Pour débuter avec un UITableViewController, le processus est direct :
- Dans votre storyboard, supprimez tout contrôleur de vue existant qui ne serait plus nécessaire.
- Faites glisser un
UITableViewControllerdepuis la bibliothèque d'objets sur votre storyboard. - Cochez la propriété "Is Initial View Controller" dans l'inspecteur d'attributs si cette table doit être la vue principale au démarrage de l'application.
- Supprimez les prototypes de cellules par défaut à l'intérieur de la
UITableViewsi vous prévoyez d'utiliser des cellules personnalisées définies dans des fichiers XIB séparés. - Créez une nouvelle classe de contrôleur (par exemple,
MyListController) qui hérite deUITableViewController. - Associez cette nouvelle classe à votre
UITableViewControllerdans le storyboard via l'inspecteur d'identité.
Il est important de noter que pour un UITableViewController, la propriété self.view est en fait une référence à self.tableView. Cela signifie que toutes les opérations que vous effectueriez normalement sur self.view peuvent être appliquées directement à self.tableView.
Personnalisation de Cellules avec XIB
Un cas d'usage courant est la création de cellules de table personnalisées pour afficher des données structurées, comme une liste d'articles ou de produits. Nous allons illustrer cela avec un exemple de cellule pour un "Produit".
- Commencez par créer un nouveau fichier XIB (par exemple,
ProductCell.xib). Glissez-y un objetUITableViewCell. - Créez une nouvelle classe Objective-C (par exemple,
ProductCell) qui hérite deUITableViewCell. - Dans le XIB, sélectionnez l'objet
UITableViewCellet définissez sa classe personnalisée surProductCelldans l'inspecteur d'identité. - Connectez les éléments de l'interface utilisateur de votre cellule (
UILabel,UIImageView, etc.) en tant qu'IBOutlets dans le fichier d'en-tête (.h) de votre classeProductCell. - Dans votre
UITableViewController, vous devrez implémenter les méthodes du protocoleUITableViewDataSource. NotammenttableView:numberOfRowsInSection:ettableView:cellForRowAtIndexPath:. - Pour optimiser les performances, définissez une hauteur de ligne par défaut via
self.tableView.rowHeight = 80;ou utiliseztableView:heightForRowAtIndexPath:pour des hauteurs dynamiques. - Vous pouvez ajuster le défilement de la table avec
self.tableView.contentInset = UIEdgeInsetsMake(20, 0, 0, 0);pour laisser un espace en haut, par exemple.
Dans votre classe ProductCell, un setter pour un objet modèle (par exemple, Product) est idéal pour configurer la vue de la cellule :
#import "ProductCell.h"
#import "Product.h" // Supposons l'existence d'une classe Product
@interface ProductCell ()
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UIImageView *productImageView;
@property (weak, nonatomic) IBOutlet UILabel *priceLabel;
@property (weak, nonatomic) IBOutlet UILabel *quantityLabel;
@end
@implementation ProductCell
- (void)setItem:(Product *)item {
_item = item;
self.titleLabel.text = item.title;
self.productImageView.image = [UIImage imageNamed:item.imageName];
self.priceLabel.text = [NSString stringWithFormat:@"%.2f €", item.price];
self.quantityLabel.text = [NSString stringWithFormat:@"%ld achetés", (long)item.purchaseCount];
}
// Méthode de commodité pour créer une instance de cellule
+ (instancetype)cellForTableView:(UITableView *)tableView {
NSString *cellIdentifier = @"ProductItemCell";
ProductCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self) owner:nil options:nil] lastObject];
}
return cell;
}
@end
Dans tableView:cellForRowAtIndexPath:, vous utiliseriez la méthode de commodité pour obtenir une cellule :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ProductCell *cell = [ProductCell cellForTableView:tableView];
cell.item = self.products[indexPath.row]; // Assigner l'objet modèle à la cellule
return cell;
}
Il est bon de savoir que les cellules initialisées par code exécutent initWithStyle:reuseIdentifier:, tandis que celles chargées depuis un XIB exécutent awakeFromNib. La méthode setSelected:animated: s'exécute lors de la sélection ou de la désélection d'une cellule, utile pour des changements visuels.
Gestion de Pied de Table (Footer View)
Un pied de table peut être utilisé pour des fonctionnalités telles que le chargement de plus de données. Imaginons un pied de table avec un bouton "Charger plus" et un indicateur d'activité.
- Créez un XIB (par exemple,
LoadingFooterView.xib) et une classeLoadingFooterViewqui hérite deUIView. - Dans le XIB, placez un
UIButtonet uneUIViewconteneur pour unUIActivityIndicatorViewet unUILabel. Connectez-les à la classeLoadingFooterView. - Implémentez une méthode d'action pour le bouton "Charger plus" et des méthodes pour contrôler la visibilité de l'indicateur d'activité.
Voici un exemple d'implémentation pour le pied de table :
// LoadingFooterView.h
#import <UIKit/UIKit.h>
@class LoadingFooterView;
@protocol LoadingFooterViewDelegate <NSObject>
@optional
- (void)footerViewDidRequestMoreData:(LoadingFooterView *)footerView;
@end
@interface LoadingFooterView : UIView
@property (nonatomic, weak) id<LoadingFooterViewDelegate> delegate;
+ (instancetype)footerViewFromXIB;
- (void)stopLoadingIndicator;
@end
// LoadingFooterView.m
#import "LoadingFooterView.h"
@interface LoadingFooterView ()
@property (weak, nonatomic) IBOutlet UIButton *loadMoreButton;
@property (weak, nonatomic) IBOutlet UIView *activityIndicatorContainer;
@end
@implementation LoadingFooterView
+ (instancetype)footerViewFromXIB {
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self) owner:nil options:nil] lastObject];
}
- (IBAction)loadMoreButtonTapped:(UIButton *)sender {
self.loadMoreButton.hidden = YES;
self.activityIndicatorContainer.hidden = NO;
if ([self.delegate respondsToSelector:@selector(footerViewDidRequestMoreData:)]) {
[self.delegate footerViewDidRequestMoreData:self];
}
}
- (void)stopLoadingIndicator {
self.loadMoreButton.hidden = NO;
self.activityIndicatorContainer.hidden = YES;
}
@end
Dans votre UITableViewController, vous configurerez le pied de table et gérerez la logique de chargement de données :
// MyListController.m
#import "MyListController.h"
#import "Product.h"
#import "ProductCell.h"
#import "LoadingFooterView.h"
@interface MyListController () <LoadingFooterViewDelegate>
@property (nonatomic, strong) NSMutableArray *products;
@end
@implementation MyListController
- (NSMutableArray *)products {
if (!_products) {
// Initialiser avec des données de test ou chargées
_products = [NSMutableArray arrayWithCapacity:5];
[_products addObject:[Product productWithTitle:@"Pomme" imageName:@"apple_icon" price:1.50 purchaseCount:150]];
[_products addObject:[Product productWithTitle:@"Orange" imageName:@"orange_icon" price:1.20 purchaseCount:200]];
// ... ajouter plus d'éléments
}
return _products;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.rowHeight = 80;
self.tableView.contentInset = UIEdgeInsetsMake(20, 0, 0, 0); // Décalage pour barre de statut
LoadingFooterView *footerView = [LoadingFooterView footerViewFromXIB];
footerView.delegate = self;
self.tableView.tableFooterView = footerView;
// Exemple d'en-tête de table
self.tableView.tableHeaderView = [[[NSBundle mainBundle] loadNibNamed:@"TableHeaderView" owner:nil options:nil] lastObject];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.products.count;
}
- (void)footerViewDidRequestMoreData:(LoadingFooterView *)footerView {
NSLog(@"Requête de données supplémentaires...");
// Simuler un appel réseau asynchrone
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSDictionary *newProductData = @{@"title":@"Banane", @"imageName":@"banana_icon", @"price":@0.99, @"purchaseCount":@300};
Product *newProduct = [Product productFromDictionary:newProductData];
[self.products addObject:newProduct];
// Insérer la nouvelle ligne avec une animation
NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:(self.products.count - 1) inSection:0];
[self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[footerView stopLoadingIndicator];
});
}
@end
Calcul de Hauteur de Cellule et Modèles de Cadre (Frame Models)
Lorsque les cellules contiennent du texte ou des images de tailles variables (comme dans un flux de médias sociaux), il est crucial de calculer dynamiquement la hauteur de chaque cellule. La méthode tableView:heightForRowAtIndexPath: est appelée avant que la cellule ne soit affichée, ce qui signifie que le calcul de la hauteur doit être effectué à l'avance.
Une approche courante consiste à utiliser un "Frame Model" (modèle de cadre). Pour chaque objet de données (par exemple, un post social), on crée un objet PostFrame qui contient les dimensions (CGRect) de tous les sous-vues de la cellule, ainsi que la hauteur totale de la cellule. Ce modèle est calculé une seule fois et stocké.
Pour le calcul de la taille du texte, la méthode boundingRectWithSize:options:attributes:context: de NSString est très utile. Elle permet de déterminer la taille qu'un bloc de texte prendra avec une taille de police et une largeur données. N'oubliez pas de définir numberOfLines = 0 pour les labels à texte variable.
Utilisation du mot-clé copy en Objective-C
Le mot-clé copy est un spécificateur de propriété important en Objective-C, particulièrement avec les objets de type chaîne de caractères (NSString) et les blocs (Block).
- Pour
NSStringetBlock: Il est recommandé d'utiliser@property (nonatomic, copy) NSString *myString;. Lorsque vous assignez une valeur à cette propriété, une nouvelle copie de l'objet est créée. Cela garantit que si la chaîne originale est uneNSMutableString(variable) et qu'elle est modifiée par la suite, votre propriétémyStringconservera sa valeur originale, car elle pointe vers une copie immuable distincte. - Pour
NSMutableString: Il ne faut PAS utilisercopypour une propriété de typeNSMutableString. Si vous faites@property (nonatomic, copy) NSMutableString *mutableString;, toute affectation à cette propriété créera une copie de typeNSString(immuable), rendant toute tentative de modification ultérieure via votre propriétémutableStringimpossible (cela provoquerait une erreur). Pour les types mutables, utilisezstrong. - Pour les objets personnalisés : Si vous souhaitez que vos propres classes prennent en charge l'opération de copie, elles doivent implémenter le protocole
NSCopyinget la méthodecopyWithZone:.
Voici un exemple de mise en œuvre de copyWithZone: pour une classe personnalisée et sa sous-classe :
// Contact.h
#import <Foundation/Foundation.h>
@interface Contact : NSObject <NSCopying> // Doit se conformer à NSCopying
@property (nonatomic, copy) NSString *fullName;
@property (nonatomic, assign) NSInteger birthYear;
+ (instancetype)contactWithDetails:(NSDictionary *)details;
@end
// Contact.m
#import "Contact.h"
@implementation Contact
+ (instancetype)contactWithDetails:(NSDictionary *)details {
Contact *newContact = [[self alloc] init];
newContact.fullName = details[@"name"];
newContact.birthYear = [details[@"year"] integerValue];
return newContact;
}
// Implémentation du protocole NSCopying
- (id)copyWithZone:(NSZone *)zone {
// Garantit que la sous-classe utilisera sa propre méthode d'allocation
Contact *copiedContact = [[[self class] allocWithZone:zone] init];
copiedContact.fullName = self.fullName;
copiedContact.birthYear = self.birthYear;
return copiedContact;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@, %p> {Nom: %@, Année de naissance: %@}",
self.class, self, self.fullName, @(self.birthYear)];
}
@end
// Employee.h
#import "Contact.h"
@interface Employee : Contact
@property (nonatomic, copy) NSString *employeeID;
@end
// Employee.m
#import "Employee.h"
@implementation Employee
- (id)copyWithZone:(NSZone *)zone {
// Appelle la méthode copy de la super-classe
Employee *copiedEmployee = [super copyWithZone:zone];
// Puis assigne les propriétés spécifiques à la sous-classe
copiedEmployee.employeeID = self.employeeID;
return copiedEmployee;
}
@end
L'utilisation de [[self class] allocWithZone:zone] dans copyWithZone: est cruciale pour assurer que si une sous-classe appelle cette méthode, une instance de la sous-classe est bien créée, et non une instance de la super-classe.
Notions de Gestion Mémoire
Historiquement, iOS n'utilisait pas de ramasse-miettes comme d'autres langages. Il s'appuyait sur le comptage de références (ARC - Automatic Reference Counting étant l'implémentation moderne). Une bonne pratique est de toujours mettre à nil les pointeurs vers des objets qui ne sont plus nécessaires ou qui ont été libérés (en environnement MRC) ou qui sont des délégués faibles, afin d'éviter des appels à des objets de-alloués (objets zombies). Par exemple, assigner myObject = nil; à un pointeur le rend sûr à manipuler, car l'envoi de messages à nil en Objective-C ne provoque pas de crash.