web-dev-qa-db-fra.com

Comment éviter les gros et maladroits UITableViewController sur iOS?

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 trouver de solution intéressante à ce problème.

De nombreuses implémentations de UITableViewController semblent assez importantes. La plupart des exemples que j'ai vus permettent au UITableViewController d'implémenter <UITableViewDelegate> et <UITableViewDataSource>. Ces implémentations sont une des principales raisons pour lesquelles UITableViewControllers'agrandit. Une solution serait de créer des classes distinctes qui implémentent <UITableViewDelegate> et <UITableViewDataSource>. Bien sûr, ces classes devraient avoir une référence au 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 similaire, en utilisant le modèle 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 des contrôleurs, car c'est l'une des pierres angulaires du modèle MVC. Mais la grande question est:

Comment devez-vous diviser le contrôleur d'une implémentation MVC en petits morceaux 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.

36
Johan Karlsson

J'évite d'utiliser UITableViewController, car cela met beaucoup de responsabilités dans un seul objet. Par conséquent, je sépare la sous-classe UIViewController de la source de données et du délégué. La responsabilité du contrôleur de vue est de préparer la vue de table, de créer une source de données avec des données et de connecter ces éléments ensemble. La modification de la façon dont la vue de table est représentée peut être effectuée sans changer le contrôleur de vue, et en effet 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, la modification du flux de travail de l'application signifie des modifications du contrôleur de vue sans se soucier de ce qui arrive à la table.

J'ai essayé de séparer les protocoles UITableViewDataSource et UITableViewDelegate en différents objets, mais cela finit généralement par être un faux fractionnement 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 la ligne 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 *)indexPath dont 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 des préoccupations de "niveau 0". Le niveau 1 est activé si je dois représenter des objets de différents types 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 …
}

Jusqu'à présent, deux solutions se sont présentées. 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'arborescence épique if() pour prendre en charge un nouveau type - il suffit d'ajouter la méthode qui prend en charge la nouvelle classe. C'est une excellente approche si cette vue de table est la seule qui doit représenter ces objets, ou doit les présenter d'une manière spéciale. Si les mêmes objets seront représentés dans des tableaux différents avec des sources de données différentes, 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 superclasse commune qui fournit ces méthodes, ou vous pouvez le faire:

@interface PhoneNumber (TableViewRepresentation)

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

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

Ensuite, 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 qui doit afficher des numéros de téléphone, des adresses, etc. peut simplement demander ce que l'objet est représenté pour une cellule de vue tabulaire. La source de données elle-même n'a plus besoin de connaître quoi que ce soit sur l'objet affiché.

"Mais attendez," j'entends une hypothétique interjection d'interlocuteur, "cela ne casse-t-il pas MVC? N'insérez-vous pas les détails de la vue dans une classe de modèle? "

Non, cela ne casse pas MVC. Vous pouvez considérer les catégories dans ce cas comme une implémentation de Decorator ; donc PhoneNumber est une classe de modèle mais PhoneNumber(TableViewRepresentation) est une catégorie de vue. La source de données (un objet contrôleur) sert d'intermédiaire entre le modèle et la vue, donc l'architecture MVC est toujours valable.

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

43
user4051

Les gens ont tendance à emballer beaucoup 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 au contrôleur de vue, car toutes les méthodes déléguées reçoivent une référence au UITableView, mais ils devront 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 vue tabulaire dans le code, pensez à les charger à la place à partir d'un fichier nib ou d'un storyboard. Les storyboards permettent le prototype et les cellules de tableau statiques - découvrez ces fonctionnalités si vous n'êtes pas familier

  • si vos méthodes déléguées contiennent beaucoup d'instructions "if" (ou instructions switch), c'est un signe classique que vous pouvez effectuer une refactorisation

Cela m'a toujours semblé un peu drôle que le UITableViewDataSource soit chargé de gérer le bon bit de données et configurer une vue pour l'afficher. Un bon point de refactoring pourrait être de changer votre cellForRowAtIndexPath pour obtenir une poignée sur les données qui doivent être affichées dans une cellule, puis déléguer la création de la vue de cellule à un autre délégué (par exemple, créer un CellViewDelegate ou similaire) qui est passé dans l'élément de données approprié.

3
occulus

Voici à peu près ce que je fais actuellement 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 comme - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;, la sous-classe remplace la méthode de chargement des données (car les applications ont généralement une sorte de méthode de chargement du cache hors-cadre qui ressemble à - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock; afin que nous puissions mettre à jour l'interface utilisateur avec les données mises en cache reçues dans LoadProgressBlock pendant que nous mettons à jour les informations du réseau, et dans le bloc d'achèvement, nous actualisons l'interface utilisateur avec de nouvelles données et supprimons les indicateurs de progression, le cas échéant). Ces classes ne sont PAS conformes au protocole UITableViewDataSource.

  • Dans BaseTableViewController (qui est conforme aux protocoles UITableViewDataSource et UITableViewDelegate), je fais référence à BaseDataSource, que je crée lors de l'initialisation du contrôleur. Dans UITableViewDataSource partie 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, donc si vous voulez une cellule personnalisée, remplacez-la également.

  • Une fois les éléments de base configurés (en fait, dans le premier projet qui utilise un tel schéma, après que cette partie peut être réutilisée), ce qui reste pour les sous-classes BaseTableViewController est:

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

    • Remplacer didSelectRowAtIndexPath: (évidemment)

    • Écrivez la sous-classe BaseDataSource qui s'occupe de travailler avec la partie nécessaire du modèle (supposons qu'il y ait 2 classes Account et Language, donc les sous-classes seront AccountDataSource et LanguageDataSource).

Et c'est tout pour la partie vue 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 a un lien vers cette question) et un article complémentaire sur les tableviewcontrollers.

2
Timur Kuchkarov

J'ai récemment écrit un article sur la façon d'implémenter des délégués et des 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 classes distinctes, comme la fabrique de cellules, la fabrique de sections et de fournir une interface générique pour le modèle que UITableView va afficher. Le schéma ci-dessous explique tout:

enter image description here

2
Piotr Wach

À mon avis, le modèle doit fournir un tableau d'objets appelés ViewModel ou viewData encapsulés dans un cellConfigurator. le CellConfigurator contient le CellInfo nécessaire pour le définir et pour configurer la cellule. il donne à la cellule des données pour que la cellule puisse se configurer elle-même. cela fonctionne aussi avec la section si vous ajoutez un objet SectionConfigurator qui contient les CellConfigurators. J'ai commencé à utiliser cela il y a quelque temps, tout d'abord en donnant à la cellule une vue Data et j'ai demandé au ViewController de retirer la cellule de la file d'attente. mais j'ai lu un article qui montrait ce dépôt gitHub.

https://github.com/fastred/ConfigurableTableViewController

cela peut changer la façon dont vous abordez cette question.

2
Pascale Beaulac

Les principes suivants SOLIDES permettront de résoudre tout type de problèmes comme ceux-ci.

Si vous voulez que vos classes aient UNE SEULE responsabilité , vous devez définir des classes DataSource et Delegate et il suffit de les injecter au propriétaire tableView (pourrait être UITableViewController ou UIViewController ou autre chose) . C'est ainsi que vous surmontez séparation des préoccupations .

Mais si vous voulez juste avoir un code propre et lisible et que vous voulez vous débarrasser de ce fichier viewController massif 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 toutes ont accès les unes aux autres. Mais cela ne résout pas vraiment le problème SoC comme je l'ai mentionné.

1
Mojtaba Hosseini