Il s'agit de la première partie d'une série d'articles sur la bibliothèque
ReactiveDataDisplayManager (RDDM) . Dans cet article, je décrirai les problèmes courants auxquels je dois faire face lorsque je travaille avec des tables «régulières», ainsi qu'une description du RDDM.

Problème 1. UITableViewDataSource
Pour commencer, oubliez la répartition des responsabilités, la réutilisation et d'autres mots sympas. Regardons le travail habituel avec les tables:
class ViewController: UIViewController { ... } extension ViewController: UITableViewDelegate { ... } extension ViewController: UITableViewDataSource { ... }
Nous analyserons l'option la plus courante. Que devons-nous mettre en œuvre? Correctement, 3 méthodes
UITableViewDataSource
sont généralement implémentées:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int func numberOfSections(in tableView: UITableView) -> Int func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
Pour l'instant, nous ne ferons pas attention aux méthodes auxiliaires (
numberOfSection
, etc.) et considérerons la plus intéressante -
func tableView(tableView: UITableView, indexPath: IndexPath)
Supposons que nous voulons remplir un tableau avec des cellules avec une description des produits, alors notre méthode ressemblera à ceci:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { let anyCell = tableView.dequeueReusableCell(withIdentifier: ProductCell.self, for: indexPath) guard let cell = anyCell as? ProductCell else { return UITableViewCell() } cell.configure(for: self.products[indexPath.row]) return cell }
Excellent, ce n'est pas difficile. Supposons maintenant que nous ayons plusieurs types de cellules, par exemple trois:
- Les produits
- Liste d'actions;
- La publicité.
Pour simplifier l'exemple, nous obtenons la méthode cell to
getCell
:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { switch indexPath.row { case 0: guard let cell: PromoCell = self.getCell() else { return UITableViewCell() } cell.configure(self.promo) return cell case 1: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.ad) return cell default: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.products[indexPath.row - 2]) return cell } }
D'une certaine manière, beaucoup de code. Imaginez que nous voulons créer l'écran des paramètres. Qu'y aura-t-il?
- Une casquette cellulaire avec un avatar;
- Un ensemble de cellules avec des transitions "en profondeur";
- Cellules avec interrupteurs (par exemple, activer / désactiver l'entrée par code PIN);
- Cellules contenant des informations (par exemple, une cellule sur laquelle il y aura un téléphone, un e-mail, etc.);
- Offres personnelles.
De plus, l'ordre est fixé. Une excellente méthode sera ...
Et maintenant, une autre situation - il existe un formulaire de saisie. Sur le formulaire d'entrée, un groupe de cellules identiques, chacune étant responsable d'un champ spécifique dans le modèle de données. Par exemple, la cellule pour entrer le téléphone est responsable du téléphone et ainsi de suite.
Tout est simple, mais il y en a un «MAIS». Dans ce cas, vous devez toujours peindre différents cas, car vous devez mettre à jour les champs nécessaires.
Vous pouvez continuer à fantasmer et à imaginer Backend Driven Design, dans lequel nous recevons 6 types de champs d'entrée différents, et selon l'état des champs (visibilité, type d'entrée, validation, valeur par défaut, etc.), les cellules changent tellement que leur ne peut pas conduire à une seule interface. Dans ce cas, cette méthode sera très désagréable. Même si vous décomposez la configuration en différentes méthodes.
Au fait, après cela, imaginez à quoi ressemblera votre code si vous souhaitez ajouter / supprimer des cellules pendant que vous travaillez. Cela ne sera pas très joli car nous serons obligés de surveiller indépendamment la cohérence des données stockées dans le
ViewController
et le nombre de cellules.
Les problèmes:
- S'il existe des cellules de types différents, le code ressemble à une nouille;
- La gestion des événements à partir des cellules pose de nombreux problèmes;
- Code laid au cas où vous auriez besoin de changer l'état de la table.
Problème 2. MindSet
Le temps des mots sympas n'est pas encore venu.
Voyons comment fonctionne l'application, ou plutôt comment les données apparaissent à l'écran. Nous présentons toujours ce processus de manière séquentielle. Eh bien, plus ou moins:
- Obtenez des données du réseau;
- Pour traiter;
- Affichez ces données à l'écran.
Mais est-ce vraiment le cas? Non! En fait, nous faisons ceci:
- Obtenez des données du réseau;
- Pour traiter;
- Enregistrer dans le modèle ViewController;
- Quelque chose provoque un rafraîchissement de l'écran;
- Le modèle enregistré est converti en cellules;
- Les données s'affichent à l'écran.
En plus de la quantité, il existe encore des différences. Premièrement, nous ne sortons plus de données, elles sont sorties. Deuxièmement, il existe une lacune logique dans le processus de traitement des données, le modèle est enregistré et le processus s'arrête là. Puis quelque chose se produit et un autre processus démarre. Ainsi, nous n'ajoutons évidemment pas d'éléments à l'écran, mais nous les sauvegardons (ce qui, soit dit en passant, est également lourd) à la demande.
Et souvenez-vous de
UITableViewDelegate
, il comprend également des méthodes pour déterminer la hauteur des cellules. Habituellement,
automaticDimension
suffit, mais parfois cela ne suffit pas et vous devez définir la hauteur vous-même (par exemple, dans le cas des animations ou des en-têtes)
Ensuite, nous partageons généralement les paramètres de cellule, la pièce avec la configuration de hauteur est dans une autre méthode.
Les problèmes:
- La connexion explicite entre le traitement des données et son affichage sur l'interface utilisateur est perdue;
- La configuration des cellules se divise en différentes parties.
Idée
Les problèmes répertoriés sur des écrans complexes provoquent des maux de tête et une forte envie d'aller prendre le thé.
Premièrement, je ne veux pas implémenter constamment des méthodes de délégué. La solution évidente est de créer un objet qui le mettra en œuvre. Ensuite, nous ferons quelque chose comme:
let displayManager = DisplayManager(self.tableView)
Super. Maintenant, vous avez besoin de l'objet pour pouvoir travailler avec toutes les cellules, tandis que la configuration de ces cellules doit être déplacée ailleurs.
Si nous mettons la configuration dans un objet séparé, nous encapsulons (il est temps pour les mots intelligents) la configuration en un seul endroit. Dans ce même endroit, nous pouvons retirer la logique de formatage des données (par exemple, changer le format de la date, la concaténation des chaînes, etc.). Grâce au même objet, nous pouvons nous abonner aux événements de la cellule.
Dans ce cas, nous aurons un objet qui a deux interfaces différentes:
- L'interface de génération d'instance
UITableView
est destinée à notre DisplayManager. - Interface d'initialisation, d'abonnement et de configuration - pour Presenter ou ViewController.
Nous appelons cet objet un générateur. Ensuite, notre générateur pour la table est une cellule, et pour tout le reste - un moyen de présenter des données sur une interface utilisateur et de traiter des événements.
Et puisque la configuration est maintenant encapsulée par le générateur, et que le générateur lui-même est une cellule, nous pouvons résoudre beaucoup de problèmes. Y compris ceux énumérés ci-dessus.
Implémentation
public protocol TableCellGenerator: class { var identifier: UITableViewCell.Type { get } var cellHeight: CGFloat { get } var estimatedCellHeight: CGFloat? { get } func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell func registerCell(in tableView: UITableView) } public protocol ViewBuilder { associatedtype ViewType: UIView func build(view: ViewType) }
Avec de telles implémentations, nous pouvons effectuer l'implémentation par défaut:
public extension TableCellGenerator where Self: ViewBuilder { func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: self.identifier.nameOfClass, for: indexPath) as? Self.ViewType else { return UITableViewCell() } self.build(view: cell) return cell as? UITableViewCell ?? UITableViewCell() } func registerCell(in tableView: UITableView) { tableView.registerNib(self.identifier) } }<source lang="swift">
Je vais donner un exemple de petit générateur:
final class FamilyCellGenerator { private var cell: FamilyCell? private var family: Family? var didTapPerson: ((Person) -> Void)? func show(family: Family) { self.family = family cell?.fill(with: family) } func showLoading() { self.family = nil cell?.showLoading() } } extension FamilyCellGenerator: TableCellGenerator { var identifier: UITableViewCell.Type { return FamilyCell.self } } extension FamilyCellGenerator: ViewBuilder { func build(view: FamilyCell) { self.cell = view view.selectionStyle = .none view.didTapPerson = { [weak self] person in self?.didTapPerson?(person) } if let family = self.family { view.fill(with: family) } else { view.showLoading() } } }
Ici, nous avons caché à la fois la configuration et les abonnements. Notez que nous avons maintenant un endroit où nous pouvons encapsuler l'état (car il est impossible d'encapsuler l'état dans la cellule car il est réutilisé par la table). Et ils ont également eu la possibilité de modifier les données dans la cellule "à la volée".
Faites attention à
self.cell = view
. Nous nous sommes souvenus de la cellule et nous pouvons maintenant mettre à jour les données sans recharger cette cellule. Ceci est une fonctionnalité utile.
Mais j'étais distrait. Comme nous pouvons avoir n'importe quelle cellule représentée par un générateur, nous pouvons rendre l'interface de notre DisplayManager un peu plus belle.
public protocol DataDisplayManager: class { associatedtype CollectionType associatedtype CellGeneratorType associatedtype HeaderGeneratorType init(collection: CollectionType) func forceRefill() func addSectionHeaderGenerator(_ generator: HeaderGeneratorType) func addCellGenerator(_ generator: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType], after: CellGeneratorType) func addCellGenerator(_ generator: CellGeneratorType, after: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType]) func update(generators: [CellGeneratorType]) func clearHeaderGenerators() func clearCellGenerators() }
Ce n'est pas tout. Nous pouvons insérer des générateurs aux bons endroits ou les supprimer.
Soit dit en passant, l'insertion d'une cellule après une cellule spécifique peut être sacrément utile. Surtout si nous chargeons progressivement les données (par exemple, l'utilisateur a entré le TIN, nous avons téléchargé les informations TIN et les avons affichées en ajoutant plusieurs nouvelles cellules après le champ TIN).
Résumé
À quoi ressemblera le travail cellulaire:
class ViewController: UIViewController { func update(data: [Products]) { let gens = data.map { ProductCellGenerator($0) } self.ddm.addGenerators(gens) } }
Ou ici:
class ViewController: UIViewController { func update(fields: [Field]) { let gens = fields.map { field switch field.type { case .phone: let gen = PhoneCellGenerator(item) gen.didUpdate = { self.updatePhone($0) } return gen case .date: let gen = DateInputCellGenerator(item) gen.didTap = { self.showPicker() } return gen case .dropdown: let gen = DropdownCellGenerator(item) gen.didTap = { self.showDropdown(item) } return gen } } let splitter = SplitterGenerator() self.ddm.addGenerator(splitter) self.ddm.addGenerators(gens) self.ddm.addGenerator(splitter) } }
Nous pouvons contrôler l'ordre d'ajout d'éléments et, en même temps, la connexion entre le traitement des données et leur ajout à l'interface utilisateur n'est pas perdue. Ainsi, dans des cas simples, nous avons un code simple. Dans les cas difficiles, le code ne se transforme pas en pâtes et en même temps semble passable. Une interface déclarative pour travailler avec les tableaux est apparue et maintenant nous encapsulons la configuration des cellules, ce qui en soi nous permet de réutiliser les cellules avec les configurations entre les différents écrans.
Avantages de l'utilisation du RDDM:
- Encapsulation de la configuration cellulaire;
- Réduction de la duplication de code en encapsulant le travail des collections vers l'adaptateur;
- Sélectionnez un objet adaptateur qui encapsule la logique spécifique de l'utilisation des collections;
- Le code devient plus évident et plus facile à lire;
- La quantité de code qui doit être écrite pour ajouter une table est réduite;
- Le processus de traitement des événements à partir des cellules est simplifié.
Sources
ici .
Merci de votre attention!