Blocs et cellules de vue tableau sur iOS


Une cellule de vue de tableau ne connaît pas la vue de tableau à laquelle elle appartient et c'est très bien. En fait, c'est comme ça que ça devrait être. Cependant, les personnes novices en la matière sont souvent déconcertées par celui-ci. Par exemple, si l'utilisateur appuie sur un bouton dans une cellule de la vue tableau, comment obtenir le chemin d'index de la cellule pour pouvoir récupérer le modèle correspondant? Dans ce tutoriel, je vais vous montrer comment ne pas faire cela, comment faire habituellement et comment le faire avec style et élégance..

1. Introduction

Lorsque l'utilisateur appuie sur une cellule de vue de tableau, celle-ci appelle tableView: didSelectRowAtIndexPath: du UITableViewDelegate protocole sur le délégué de la vue table. Cette méthode accepte deux arguments, la vue tableau et le chemin d'index de la cellule sélectionnée..

Le problème que nous allons aborder dans ce tutoriel est toutefois un peu plus complexe. Supposons que nous ayons une vue sous forme de tableau avec des cellules, chaque cellule contenant un bouton. Lorsque vous appuyez sur le bouton, une action est déclenchée. Dans l'action, nous devons extraire le modèle qui correspond à la position de la cellule dans la vue tableau. En d'autres termes, nous devons connaître le chemin d'index de la cellule. Comment pouvons-nous déduire le chemin d'index de la cellule si nous obtenons uniquement une référence au bouton qui a été exploité? C'est le problème que nous allons résoudre dans ce tutoriel.

2. Configuration du projet

Étape 1: Créer un projet

Créez un nouveau projet dans Xcode en sélectionnant le Application à vue unique modèle de la liste des Application iOS modèles. Nommez le projet Blocs et cellules, ensemble Dispositifs à iPhone, et cliquez Suivant. Indiquez à Xcode où vous souhaitez stocker le projet et appuyez sur Créer.

Étape 2: Mettre à jour la cible de déploiement

Ouvrez le Navigateur de projet à gauche, sélectionnez le projet dans le Projet section, et définir la Cible de déploiement à iOS 6. Nous faisons cela pour nous assurer que nous pouvons exécuter l'application sur iOS 6 et iOS 7. La raison en sera expliquée plus tard dans ce tutoriel..

Étape 3: Créer UITableViewCell Sous-classe

Sélectionner Nouveau> Fichier… du Fichier menu et choisir Classe Objective-C de la liste des Cacao Touch modèles. Nommez la classe TPSButtonCell et assurez-vous qu'il hérite de UITableViewCell.

Ouvrez le fichier d’en-tête de la classe et déclarez deux prises, une UILabel instance nommée titleLabel et un UIButton instance nommée actionButton.

 #importation  @interface TPSButtonCell: UITableViewCell @property (faible, non atomique) IBOutlet UILabel * titleLabel; @property (faible, non atomique) IBOutlet UIButton * actionButton; @fin

Étape 4: Mettre à jour le contrôleur de vue

Ouvrez le fichier d'en-tête du TPSViewController classe et créer un point de vente nommé tableView de type UITableView. le TPSViewController doit également adopter le UITableViewDataSource et UITableViewDelegate les protocoles.

 #importation  @interface TPSViewController: UIViewController  @property (faible, non atomique) IBOutlet UITableView * tableView; @fin

Nous devons également examiner brièvement le fichier d'implémentation du contrôleur de vue. Ouvrez TPSViewController.m et déclarez une variable statique de type. NSString que nous utiliserons comme identifiant de réutilisation pour les cellules dans la vue tableau.

 #import "TPSViewController.h" @implementation TPSViewController statique NSString * CellIdentifier = @ "CellIdentifier"; //… // @fin

Étape 5: Interface utilisateur

Ouvrez le scénariseur principal du projet, Main.Storyboard, et faites glisser une vue de tableau vers la vue du contrôleur de vue. Sélectionnez la vue table et connectez-la la source de données et déléguer prises avec l'instance de contrôleur de vue. La vue de tableau étant toujours sélectionnée, ouvrez le Inspecteur d'attributs et définir le nombre de Cellules Prototype à 1. le Contenu attribut doit être réglé sur Prototypes dynamiques. Vous devriez maintenant voir un prototype de cellule dans la vue tableau.

Sélectionnez la cellule prototype et définissez son Classe à TPSButtonCell dans le Inspecteur d'identité. La cellule étant toujours sélectionnée, ouvrez le Inspecteur d'attributs et mettre le Style attribuer à Douane et le Identifiant à CellIdentifier.

Faites glisser un UILabel exemple de la Bibliothèque d'objets à la vue de contenu de la cellule et répétez cette étape pour un UIButton exemple. Sélectionnez la cellule, ouvrez le Inspecteur de connexions, et connectez le titleLabel et actionButton points de vente avec leurs homologues dans la cellule prototype.

Avant de replonger dans le code, nous devons établir une connexion supplémentaire. Sélectionnez le contrôleur de vue, ouvrez le Inspecteur de connexions une fois de plus, et connectez le contrôleur de vue tableView sortie avec la vue de la table dans le storyboard. Voilà pour l'interface utilisateur.

3. Remplir la vue tableau

Étape 1: créer une source de données

Peupler la vue de table avec quelques films notables qui ont été publiés en 2013. Dans le TPSViewController classe, déclarer une propriété de type NSArray et nommez-le la source de données. La variable d'instance correspondante contiendra les films que nous montrerons dans la vue tableau. Peupler la source de données avec une douzaine de films dans le contrôleur de vue viewDidLoad méthode.

 #import "TPSViewController.h" @interface TPSViewController () @property (fort, non atomique) NSArray * dataSource; @fin
 - (void) viewDidLoad [super viewDidLoad]; // Configuration de la source de données self.dataSource = @ [@ @ "title": @ "Gravity", @ "year": @ (2013), @ @ "title": @ "12 Years a Slave", @ "année": @ (2013), @ @ "titre": @ "Avant minuit", @ "année": @ (2013), @ @ "titre": @ "American Hustle", @ "année ": @ (2013), @ @" titre ": @" Blackfish ", @" année ": @ (2013), @ @" titre ": @" Capitaine Phillips ", @" année ": @ (2013), @ @ "titre": @ "Nebraska", @ "année": @ (2013), @ @ "titre": @ "Rush", @ "année": @ (2013) , @ @ "title": @ "Frozen", @ "year": @ (2013), @ @ "title": @ "Star Trek Into Darkness", @ "year": @ (2013), @ @ "title": @ "The Conjuring", @ "year": @ (2013), @ @ "title": @ "Side Effects", @ "year": @ (2013), @  @ "titre": @ "L'attaque", @ "année": @ (2013), @ @ "titre": @ "Le Hobbit", @ "année": @ (2013), @ @ " titre ": @" nous sommes ce que nous sommes ", @" année ": @ (2013), @ @" titre ": @" quelque chose dans l'air ", @" année ": @ (2013)]; 

Étape 2: Mettre en œuvre le UITableViewDataSource Protocole

La mise en œuvre de la UITableViewDataSource le protocole est très facile. Nous avons seulement besoin de mettre en œuvre numberOfSectionsInTableView:, tableView: numberOfRowsInSection:, et tableView: cellForRowAtIndexPath:.

 - (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView return self.dataSource? dix;  - (NSInteger) tableView: (UITableView *) tableView numberOfRowsInSection: (NSInteger) section return self.dataSource? self.dataSource.count: 0;  - (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath TPSButtonCell * cell = (TPSButtonCell *) [tableView dequeueReusableCellWithIdentifier: CellIdentifier pourIndexPath // Récupération de l'élément NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Configurer la cellule de la vue tableau [cell.titleLabel setText: [NSString stringWithFormat: @ "% @ (% @)", item [@ "title"], item [@ "year"]]; [cell.actionButton addTarget: action propre: @selector (didTapButton :) pourControlEvents: UIControlEventTouchUpInside]; cellule de retour; 

Dans tableView: cellForRowAtIndexPath:, nous utilisons le même identifiant que celui que nous avons défini dans le scénarimage principal, CellIdentifier, que nous avons déclaré plus tôt dans le tutoriel. Nous lançons la cellule sur une instance de TPSButtonCell, récupérez l'élément correspondant dans la source de données et mettez à jour le titre de la cellule. Nous ajoutons également une cible et une action pour le UIControlEventTouchUpInside événement du bouton.

N'oubliez pas d'ajouter une déclaration d'importation pour le TPSButtonCell classe en haut de TPSViewController.m.

 #import "TPSButtonCell.h"

Pour empêcher l’application de se bloquer lorsqu’un bouton est tapé, implémentez didTapButton: comme indiqué ci-dessous.

 - (void) didTapButton: (id) expéditeur NSLog (@ "% s", __PRETTY_FUNCTION__); 

Générez le projet et exécutez-le dans le simulateur iOS pour voir ce que nous avons jusqu'à présent. Vous devriez voir une liste de films et appuyer sur le bouton à droite pour enregistrer un message sur la console Xcode. Génial. C'est l'heure du tutoriel.

4. Comment ne pas le faire

Lorsque l'utilisateur appuie sur le bouton à droite, il envoie un message de didTapButton: au contrôleur de vue. Vous devez presque toujours connaître le chemin d'index de la cellule de la table dans laquelle se trouve le bouton. Mais comment obtenir ce chemin d'index? Comme je l'ai mentionné, il y a trois approches possibles. Voyons d'abord comment ne pas le faire.

Jetez un coup d’œil à la mise en œuvre de didTapButton: et essayer de découvrir ce qui ne va pas avec elle. Repères-tu le danger? Laissez-moi vous aider. Exécutez d'abord l'application sur iOS 7, puis sur iOS 6. Jetez un coup d'œil à ce que Xcode envoie à la console..

 - (void) didTapButton: (id) sender // Rechercher une cellule de vue de tableau UITableViewCell * cell = (UITableViewCell *) [[[sender superview] superview] superview]; // Infer Index Path NSIndexPath * indexPath = [self.tableView indexPathForCell: cellule]; // Récupération de l'élément NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Se connecter à la console NSLog (@ "% @", item [@ "title"]); 

Le problème avec cette approche est qu’elle est sujette aux erreurs. Sur iOS 7, cette approche fonctionne très bien. Sur iOS 6, cependant, cela ne fonctionne pas. Pour que cela fonctionne sur iOS 6, vous devez implémenter la méthode comme indiqué ci-dessous. La hiérarchie de vues d'un certain nombre de communs UIView sous-classes, telles que UITableView, a changé dans iOS 7 et le résultat est que l'approche ci-dessus ne produit pas un résultat cohérent.

 - (void) didTapButton: (id) sender // Rechercher une cellule de vue de tableau UITableViewCell * cell = (UITableViewCell *) [[sender superview] superview]; // Infer Index Path NSIndexPath * indexPath = [self.tableView indexPathForCell: cellule]; // Récupération de l'élément NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Se connecter à la console NSLog (@ "% @", item [@ "title"]); 

Ne pouvons-nous pas simplement vérifier si l'appareil exécute iOS 7? C'est une très bonne idée. Cependant, que ferez-vous lorsque iOS 8 modifiera la hiérarchie de la vue interne de UITableView encore? Allez-vous patcher votre application chaque fois qu'une version majeure d'iOS est introduite? Et qu'en est-il de tous les utilisateurs qui ne mettent pas à niveau la dernière version (corrigée) de votre application? J'espère qu'il est clair que nous avons besoin d'une meilleure solution.

5. Une meilleure solution

Une meilleure approche consiste à déduire le chemin d’index de la cellule dans la vue tableau en fonction de la position du expéditeur, la UIButton exemple, dans la vue tableau. Nous utilisons convertPoint: toView: pour y parvenir. Cette méthode convertit le centre du bouton du système de coordonnées du bouton au système de coordonnées de la vue Table. Cela devient alors très facile. Nous appelons indexPathForRowAtPoint: sur la table voir et passer pointInSuperview à cela. Cela nous donne un chemin d’index que nous pouvons utiliser pour extraire l’élément correct de la source de données..

 - (void) didTapButton: (id) sender // Transmettre l'expéditeur vers UIButton UIButton * button = (UIButton *) expéditeur; // Trouver un point dans la vue d'ensemble CGPoint pointInSuperview = [button.superview Aperçu convertPoint: button.center toView: self.tableView]; // Infer Index Path NSIndexPath * indexPath = [self.tableView indexPathForRowAtPoint: pointInSuperview]; // Récupération de l'élément NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Se connecter à la console NSLog (@ "% @", item [@ "title"]); 

Cette approche peut sembler lourde, mais en réalité elle ne l’est pas. C'est une approche qui n'est pas affectée par les changements dans la hiérarchie de vues de UITableView et il peut être utilisé dans de nombreux scénarios, y compris dans les vues de collection.

6. La solution élégante

Il existe une solution supplémentaire pour résoudre le problème, qui nécessite un peu plus de travail. Le résultat, cependant, est un affichage de l'Objective-C moderne. Commencez par consulter le fichier d’en-tête du TPSButtonCell et déclarer une méthode publique nommée setDidTapButtonBlock: qui accepte un bloc.

 #importation  @interface TPSButtonCell: UITableViewCell @property (faible, non atomique) IBOutlet UILabel * titleLabel; @property (faible, non atomique) IBOutlet UIButton * actionButton; - (void) setDidTapButtonBlock: (void (^) (expéditeur id)) didTapButtonBlock; @fin

Dans le dossier de mise en oeuvre de TPSButtonCell créer une propriété privée nommée didTapButtonBlock comme indiqué ci-dessous. Notez que la propriété attribuée est définie sur copie, parce que les blocs doivent être copiés pour garder une trace de leur état capturé en dehors de la portée d'origine.

 #import "TPSButtonCell.h" @interface TPSButtonCell () @property (copie non atomique) void (^ didTapButtonBlock) (id expéditeur); @fin

Au lieu d’ajouter une cible et une action pour le UIControlEventTouchUpInside événement dans le contrôleur de vue tableView: cellForRowAtIndexPath:, nous ajoutons une cible et une action dans awakeFromNib dans le TPSButtonCell classe elle-même.

 - (vide) awakeFromNib [super awakeFromNib]; [self.actionButton addTarget: auto action: @selector (didTapButton :) pourControlEvents: UIControlEventTouchUpInside]; 

L'implémentation de didTapButton: est trivial.

 - (void) didTapButton: (id) expéditeur if (self.didTapButtonBlock) self.didTapButtonBlock (expéditeur); 

Cela peut sembler beaucoup de travail pour un simple bouton, mais tenez vos chevaux jusqu'à ce que nous ayons refactorisé tableView: cellForRowAtIndexPath: dans le TPSViewController classe. Au lieu d'ajouter une cible et une action au bouton de la cellule, nous définissons la cellule didTapButtonBlock. Obtenir une référence à l'élément correspondant de la source de données devient très, très facile. Cette solution est de loin la solution la plus élégante à ce problème.

 - (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath TPSButtonCell * cell = (TPSButtonCell *) [tableView dequeueReusableCellWithIdentifier: CellIdentifier pourIndexPath: // Récupération de l'élément NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Configurer la cellule de la vue tableau [cell.titleLabel setText: [NSString stringWithFormat: @ "% @ (% @)", item [@ "title"], item [@ "year"]]; [cell setDidTapButtonBlock: ^ (expéditeur de l'identifiant) NSLog (@ "% @", élément [@ "titre"]); ]; cellule de retour; 

Conclusion

Même si le concept de blocs existe depuis des décennies, les développeurs de Cocoa ont dû attendre jusqu'en 2011. Les blocs peuvent faciliter la résolution de problèmes complexes et simplifier le code complexe. Depuis l'introduction des blocs, Apple en a largement fait usage dans ses propres API. Je vous encourage donc à suivre l'exemple d'Apple en tirant parti des blocs dans vos propres projets..