Comment éviter UITableViewController sur iOS?

36

J'ai un problème lors de l'implémentation du modèle MVC sur iOS. J'ai cherché sur Internet mais ne semble pas avoir trouvé de solution satisfaisante à ce problème.

De nombreuses UITableViewControllerimplémentations semblent être plutôt grandes. La plupart des exemples que j'ai vus permettent de UITableViewControllermettre en œuvre <UITableViewDelegate>et <UITableViewDataSource>. Ces mises en œuvre sont l'une des principales raisons pour lesquelles il UITableViewControllerdevient gros. Une solution serait de créer des classes séparées qui implémentent <UITableViewDelegate>et <UITableViewDataSource>. Bien sûr, ces classes devraient faire référence à la UITableViewController. Y a-t-il des inconvénients à utiliser cette solution? En général, je pense que vous devriez déléguer la fonctionnalité à d'autres classes "Helper" ou similaires, en utilisant le modèle de délégué. Existe-t-il des moyens bien établis de résoudre ce problème?

Je ne veux pas que le modèle contienne trop de fonctionnalités, ni la vue. Je crois que la logique devrait vraiment être dans la classe du contrôleur, car c'est l'une des pierres angulaires du modèle MVC. Mais la grande question est:

Comment diviser le contrôleur d'une implémentation MVC en de plus petites pièces gérables? (S'applique à MVC dans iOS dans ce cas)

Il pourrait y avoir un schéma général pour résoudre ce problème, bien que je recherche spécifiquement une solution pour iOS. Veuillez donner un exemple d’un bon modèle pour résoudre ce problème. Veuillez expliquer pourquoi votre solution est géniale.

Johan Karlsson
la source
1
"C'est aussi un argument pour lequel cette solution est géniale." :)
occulus
1
C'est un peu à côté du sujet, mais la UITableViewControllermécanique me semble assez étrange, donc je peux comprendre le problème. Je suis en fait heureux usage I MonoTouch, car en MonoTouch.Dialogfait expressément que beaucoup plus facile de travailler avec des tables sur iOS. En attendant, je suis curieux de savoir ce que pourraient suggérer d'autres personnes plus
expérimentées

Réponses:

43

J'évite d'utiliser UITableViewController, car cela confère beaucoup de responsabilités à un seul objet. Par conséquent, je sépare la UIViewControllersous - classe de la source de données et le délégué. La responsabilité du contrôleur de vue est de préparer la vue tableau, de créer une source de données avec des données et de lier ces éléments entre eux. Il est possible de changer la façon dont la table est représentée sans changer de contrôleur de vue, et le même contrôleur de vue peut être utilisé pour plusieurs sources de données qui suivent toutes ce modèle. De même, modifier le flux de travail de l'application signifie modifier le contrôleur de vue sans se soucier de ce qu'il advient de la table.

J'ai essayé de séparer les protocoles UITableViewDataSourceet UITableViewDelegatedans des objets différents, mais cela finit généralement par être une fausse division car presque toutes les méthodes du délégué doivent creuser dans la source de données (par exemple, lors de la sélection, le délégué doit savoir quel objet est représenté par le rangée sélectionnée). Je me retrouve donc avec un seul objet qui est à la fois la source de données et le délégué. Cet objet fournit toujours une méthode -(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPathdont la source de données et les aspects délégués ont besoin de savoir sur quoi ils travaillent.

C'est ma séparation de niveau "niveau 0". Le niveau 1 est engagé si je dois représenter des objets de types différents dans la même vue de table. Par exemple, imaginez que vous deviez écrire l'application Contacts: pour un seul contact, vous pourriez avoir des lignes représentant des numéros de téléphone, d'autres lignes représentant des adresses, d'autres représentant des adresses e-mail, etc. Je veux éviter cette approche:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  if ([object isKindOfClass: [PhoneNumber class]]) {
    //configure phone number cell
  }
  else if …
}

Deux solutions se sont présentées jusqu'à présent. L'une consiste à construire dynamiquement un sélecteur:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  NSString *cellSelectorName = [NSString stringWithFormat: @"tableView:cellFor%@AtIndexPath:", [object class]];
  SEL cellSelector = NSSelectorFromString(cellSelectorName);
  return [self performSelector: cellSelector withObject: tableView withObject: object];
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForPhoneNumberAtIndexPath: (NSIndexPath *)indexPath {
  // configure phone number cell
}

Dans cette approche, vous n'avez pas besoin de modifier l' if()arborescence épique pour prendre en charge un nouveau type. Ajoutez simplement la méthode prenant en charge la nouvelle classe. C'est une excellente approche si cette vue sous forme de tableau est la seule qui doit représenter ces objets, ou doit les présenter de manière spéciale. Si les mêmes objets doivent être représentés dans différentes tables avec différentes sources de données, cette approche échoue, car les méthodes de création de cellules doivent être partagées entre les sources de données. Vous pouvez définir une super-classe commune fournissant ces méthodes, ou procédez comme suit:

@interface PhoneNumber (TableViewRepresentation)

- (UITableViewCell *)tableView: (UITableView *)tableView representationAsCellForRowAtIndexPath: (NSIndexPath *)indexPath;

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

Puis dans votre classe de source de données:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  return [object tableView: tableView representationAsCellForRowAtIndexPath: indexPath];
}

Cela signifie que toute source de données ayant besoin d'afficher des numéros de téléphone, des adresses, etc. peut simplement demander quel objet est représenté pour une cellule d'affichage de tableau. La source de données elle-même n'a plus besoin de rien savoir de l'objet affiché.

"Mais attendez," j'entends un interlocuteur hypothétique, "ça ne rompt pas MVC? Ne mettez-vous pas les détails de vue dans une classe modèle?"

Non, ça ne casse pas MVC. Vous pouvez considérer les catégories dans ce cas comme une implémentation de Decorator ; Il en PhoneNumberva de même d’une classe de modèle, mais d’ PhoneNumber(TableViewRepresentation)une catégorie de vue. La source de données (un objet contrôleur) sert de médiateur entre le modèle et la vue, de sorte que l'architecture MVC est toujours valable.

Vous pouvez également voir cette utilisation des catégories comme décoration dans les cadres d’Apple. NSAttributedStringest une classe modèle contenant du texte et des attributs. AppKit fournit NSAttributedString(AppKitAdditions)et UIKit fournit des NSAttributedString(NSStringDrawing)catégories de décorateur qui ajoutent un comportement de dessin à ces classes de modèle.


la source
Quel est le bon nom pour la classe qui fonctionne en tant que délégué de la source de données et de la vue table?
Johan Karlsson
1
@JohanKarlsson Je l'appelle souvent simplement la source de données. C'est peut-être un peu bâclé, mais je combine souvent les deux pour savoir que ma "source de données" est une adaptation de la définition plus restreinte d'Apple.
1
Cet article: objc.io/issue-1/table-views.html propose un moyen de gérer plusieurs types de cellules, ce qui vous permet de définir la classe de cellules dans la cellForPhotoAtIndexPathméthode de la source de données, puis d'appeler une méthode de fabrique appropriée. Ce qui, bien entendu, n’est possible que si des classes particulières occupent de manière prévisible des lignes particulières. Votre système de génération de vues sur les modèles est beaucoup plus élégant dans la pratique, même s'il s'agit peut-être d'une approche peu orthodoxe de MVC! :)
Benji XVI
1
J'ai essayé de faire la démonstration de ce modèle sur github.com/yonglam/TableViewPattern . J'espère que c'est utile pour quelqu'un.
Andrew
1
Je voterai un non définitif pour l'approche du sélecteur dynamique. C'est très dangereux car les problèmes ne se manifestent qu'au moment de l'exécution. Il n'y a pas de moyen automatisé de s'assurer que le sélecteur donné existe et qu'il est dactylographié correctement. Ce type d'approche finira par s'effondrer et constitue un cauchemar à maintenir. L'autre approche, cependant, est très intelligente.
mkko
3

Les gens ont tendance à accumuler beaucoup de choses dans UIViewController / UITableViewController.

La délégation à une autre classe (pas le contrôleur de vue) fonctionne généralement bien. Les délégués n'ont pas nécessairement besoin d'une référence vers le contrôleur de vue, car toutes les méthodes de délégation reçoivent une référence à UITableView, mais ils auront besoin d'accéder d'une manière ou d'une autre aux données pour lesquelles ils délèguent.

Quelques idées de réorganisation pour réduire la longueur:

  • Si vous construisez les cellules de la vue tableau dans le code, envisagez de les charger à partir d'un fichier nib ou d'un storyboard. Les storyboards permettent d'utiliser des cellules de prototype et de table statique - vérifiez ces fonctionnalités si vous ne les connaissez pas.

  • si vos méthodes de délégué contiennent beaucoup d'instructions 'if' (ou d'instructions switch), c'est un signe classique de la possibilité de refactoriser

Cela m'a toujours semblé un peu amusant de penser qu’il UITableViewDataSourceétait responsable d’obtenir le contrôle du bit de données correct et de configurer une vue pour le montrer. Un bon point de refactoring pourrait être de changer votre cellForRowAtIndexPathpour obtenir un handle sur les données qui doivent être affichées dans une cellule, puis de déléguer la création de la vue de cellule à un autre délégué (par exemple make a CellViewDelegateou similaire) qui est passé dans l'élément de données approprié.

occulus
la source
C'est une bonne réponse. Cependant, quelques questions se posent dans ma tête. Pourquoi trouvez-vous qu'un grand nombre d'instructions-if (ou d'instructions-commutateurs) mal conçues? Voulez-vous dire en réalité beaucoup d’instructions if et switch imbriquées? Comment re-factoriser pour éviter les déclarations if ou switch?
Johan Karlsson
@JohanKarlsson une technique consiste à utiliser le polymorphisme. Si vous avez besoin de faire une chose avec un type d'objet et quelque chose avec un type différent, transformez ces objets en classes différentes et laissez-les choisir le travail à votre place.
@GrahamLee Oui, je connais le polymorphisme ;-) Cependant, je ne sais pas comment l'appliquer dans ce contexte. S'il vous plaît élaborer sur ce sujet.
Johan Karlsson
@JohanKarlsson done;)
2

Voici à peu près ce que je suis en train de faire face à un problème similaire:

  • Déplacez les opérations liées aux données vers la classe XXXDataSource (qui hérite de BaseDataSource: NSObject). BaseDataSource fournit des méthodes pratiques, telles que - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;, la sous-classe substitue la méthode de chargement des données (les applications ayant généralement une sorte de méthode de chargement en cache offlie, - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;nous permettant de mettre à jour l'interface utilisateur avec les données en cache reçues dans LoadProgressBlock pendant la mise à jour des informations depuis le réseau et dans le bloc d'achèvement. nous actualisons l'interface utilisateur avec les nouvelles données et supprimons les indicateurs de progression, le cas échéant). Ces classes ne sont pas conformes au UITableViewDataSourceprotocole.

  • Dans BaseTableViewController (qui est conforme à UITableViewDataSourceet aux UITableViewDelegateprotocoles), j'ai une référence à BaseDataSource, que je crée pendant l'initialisation du contrôleur. Dans une UITableViewDataSourcepartie du contrôleur, je renvoie simplement les valeurs de DataSource (comme - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; }).

Voici mon cellForRow dans la classe de base (pas besoin de remplacer dans les sous-classes):

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *cellIdentifier = [NSString stringWithFormat:@"%@%@", NSStringFromClass([self class]), @"TableViewCell"];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [self createCellForIndexPath:indexPath withCellIdentifier:cellIdentifier];
    }
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

configureCell doit être remplacé par des sous-classes et createCell renvoie UITableViewCell. Si vous souhaitez une cellule personnalisée, remplacez-la également.

  • Une fois les éléments de base configurés (en fait, dans le premier projet utilisant un tel schéma, cette partie peut ensuite être réutilisée). Ce qui reste pour les BaseTableViewControllersous-classes est le suivant:

    • Remplace configureCell (cela se transforme généralement en demandant à dataSource pour un objet de chercher le chemin d'index et en l'envoyant à la méthode configureWithXXX de la cellule ou en obtenant la représentation UITableViewCell de l'objet comme dans la réponse de l'utilisateur4051)

    • Remplacer didSelectRowAtIndexPath: (évidemment)

    • Ecrit la sous-classe BaseDataSource qui s’occupe de travailler avec la partie nécessaire de Model (supposons qu’il existe 2 classes Accountet Language, par conséquent, les sous-classes seront AccountDataSource et LanguageDataSource).

Et c'est tout pour la partie vue de table. Je peux poster du code sur GitHub si nécessaire.

Edit: quelques recommandations peuvent être trouvées sur http://www.objc.io/issue-1/lighter-view-controllers.html (qui contient un lien vers cette question) et un article associé sur tableviewcontrollers.

Timur Kuchkarov
la source
2

Mon point de vue est que le modèle doit donner un tableau d'objets appelés ViewModel ou viewData encapsulés dans un cellConfigurator. le CellConfigurator détient le CellInfo nécessaire pour l'exécuter et pour configurer la cellule. il donne des données à la cellule afin que celle-ci puisse se configurer elle-même. cela fonctionne aussi avec section si vous ajoutez un objet SectionConfigurator contenant les CellConfigurators. Au début, je commençais à utiliser cette méthode tout en donnant à la cellule un viewData. mais j'ai lu un article qui pointait vers ce dépôt gitHub.

https://github.com/fastred/ConfigurableTableViewController

cela peut changer votre façon de voir les choses.

Pascale Beaulac
la source
2

J'ai récemment écrit un article sur la mise en oeuvre de délégués et de sources de données pour UITableView: http://gosuwachu.gitlab.io/2014/01/12/uitableview-controller/

L'idée principale est de diviser les responsabilités en plusieurs classes, telles qu'une fabrique de cellules, une fabrique de sections, et de fournir une interface générique pour le modèle que UITableView va afficher. Le diagramme ci-dessous explique tout:

entrez la description de l'image ici

Piotr Wach
la source
Ce lien ne fonctionne plus.
Koen
1

Suivre les principes de SOLID résoudra tout type de problèmes de ce type.

Si vous voulez vos classes d'avoir une seule responsabilité, vous devez définir séparément DataSourceet les Delegateclasses et simplement injecter eux au tableViewpropriétaire (peut - être UITableViewControllerou UIViewControllerou quoi que ce soit d' autre). Voici comment vous surmontez la séparation des préoccupations .

Mais si vous voulez juste avoir un code propre et lisible et que vous voulez vous débarrasser de cet énorme fichier viewController et que vous êtes dans Swif , vous pouvez utiliser extensions pour cela. Les extensions de la classe unique peuvent être écrites dans différents fichiers et tous ont accès les uns aux autres. Mais c’est vraiment ce qui résout le problème des SoC comme je l’ai mentionné.

Mojtaba Hosseini
la source