Objet composite «source de données» et éléments d'une approche fonctionnelle

Une fois, j'ai (enfin, pas même moi) fait face à la tâche d'ajouter une cellule d'un type complètement différent à l' UICollectionView avec un certain type de cellules, et de le faire uniquement dans un cas spécial, qui est traité "ci-dessus" et ne dépend pas directement de l' UICollectionView . Cette tâche a donné lieu, si ma mémoire est UICollectionViewDataSource , à quelques vilains blocs if - else à l'intérieur des UICollectionViewDelegate UICollectionViewDataSource et UICollectionViewDelegate , qui se sont installés en toute sécurité dans le code "production" et, probablement, n'iront nulle part à partir de là.

Dans le cadre de la tâche susmentionnée, il était inutile de réfléchir à une solution plus élégante et de ne gaspiller aucune énergie «réfléchie» à ce sujet. Néanmoins, je me suis souvenu de cette histoire: j'ai pensé à essayer d'implémenter un certain objet «source de données», qui pourrait être composé d'un certain nombre d'autres objets «source de données» en un seul ensemble. La solution doit évidemment être généralisée, adaptée à n'importe quel nombre de composants (y compris zéro et un) et ne pas dépendre de types spécifiques. Il s'est avéré que ce n'est pas seulement réel, mais aussi pas trop difficile (bien que rendre le code aussi «beau» soit un peu plus difficile).

Je vais montrer ce que j'ai fait avec l'exemple UITableView . Si vous le souhaitez, l'écriture d'un code similaire pour UICollectionView ne devrait pas être difficile.

"Une idée est toujours plus importante que son incarnation"


Cet aphorisme appartient au grand auteur de bandes dessinées Alan Moore ( «Keepers », «V signifie Vendetta », «League of Outstanding Gentlemen» ), mais ce n'est pas vraiment une question de programmation, non?

L'idée principale de mon approche est de stocker un tableau d'objets UITableViewDataSource , de renvoyer leur nombre total de sections et de pouvoir déterminer lors de l'accès à la section lequel des objets "datasource" d'origine redirigera cet appel.

Le protocole UITableViewDataSource a déjà les méthodes nécessaires pour obtenir le nombre de sections, de lignes, etc., mais, malheureusement, dans ce cas, je l'ai trouvé extrêmement gênant à utiliser en raison de la nécessité de transmettre une référence à une instance spécifique de UITableView comme l'un des arguments. Par conséquent, j'ai décidé d'étendre le protocole UITableViewDataSource standard avec quelques membres simples supplémentaires:

 protocol ComposableTableViewDataSource: UITableViewDataSource { var numberOfSections: Int { get } func numberOfRows(for section: Int) -> Int } 

Et la «source de données» composite s'est avérée être une classe simple qui implémente les exigences de UITableViewDataSource et est initialisée avec un seul argument - un ensemble d'instances spécifiques de ComposableTableViewDataSource :

 final class ComposedTableViewDataSource: NSObject, UITableViewDataSource { private let dataSources: [ComposableTableViewDataSource] init(dataSources: ComposableTableViewDataSource...) { self.dataSources = dataSources super.init() } private override init() { fatalError("\(#file) \(#line): Initializer with parameters must be used.") } } 

Il ne reste plus qu'à écrire les implémentations de toutes les méthodes du protocole UITableViewDataSource afin qu'elles se réfèrent aux méthodes des composants correspondants.

«C'était la bonne décision. Ma décision


Ces mots appartenaient à Boris Nikolaïevitch Eltsine , le premier président de la Fédération de Russie , et ne font pas vraiment référence au texte ci-dessous, je les ai juste aimés.

La bonne décision m'a semblé utiliser les fonctionnalités du langage Swift , et cela s'est avéré vraiment pratique.

Tout d'abord, nous implémentons une méthode qui renvoie le nombre de sections - ce n'est pas difficile. Comme mentionné ci-dessus, nous avons juste besoin du nombre total de toutes les sections des composants:

 func numberOfSections(in tableView: UITableView) -> Int { // Default value if not implemented is "1". return dataSources.reduce(0) { $0 + ($1.numberOfSections?(in: tableView) ?? 1) } } 

(Je n'expliquerai pas la syntaxe et la signification des fonctions standard. Si nécessaire, Internet regorge de bons articles d' introduction sur le sujet . Et je peux également recommander un assez bon livre .)

Un rapide coup d'œil à toutes les méthodes de UITableViewDataSource , vous remarquerez qu'en tant qu'arguments, ils n'acceptent qu'un lien vers la table et la valeur du numéro de section ou de la ligne IndexPath correspondante. Nous écrirons quelques assistants qui nous seront utiles pour implémenter toutes les autres méthodes de protocole.

Tout d'abord, toutes les tâches peuvent être réduites à une fonction "générique" , qui prend comme arguments une référence à un ComposableTableViewDataSource spécifique et la valeur du numéro de section ou IndexPath . Pour plus de commodité et de brièveté, nous attribuons des pseudonymes aux types de ces fonctions. De plus, pour plus de lisibilité, je suggère de déclarer un alias pour le numéro de section:

 private typealias SectionNumber = Int private typealias AdducedSectionTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ sectionNumber: SectionNumber) -> T private typealias AdducedIndexPathTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ indexPath: IndexPath) -> T 

(Je vais expliquer les noms sélectionnés juste en dessous.)

Deuxièmement, nous implémentons une fonction simple qui détermine le ComposableTableViewDataSource spécifique et le numéro de section correspondant par le numéro de section ComposedTableViewDataSource :

 private func decompose(section: SectionNumber) -> (dataSource: ComposableTableViewDataSource, decomposedSection: SectionNumber) { var section = section var dataSourceIndex = 0 for (index, dataSource) in dataSources.enumerated() { let diff = section - dataSource.numberOfSections dataSourceIndex = index if diff < 0 { break } else { section = diff } } return (dataSources[dataSourceIndex], section) } 

Peut-être, si vous pensez un peu plus longtemps que le mien, la mise en œuvre se révélera plus élégante et moins simple. Par exemple, des collègues ont immédiatement suggéré que j'implémente une recherche binaire dans cette fonction (auparavant, par exemple, lors de l'initialisation, en composant l'index du nombre de sections - un simple tableau d' entiers ). Ou même passez un peu de temps à compiler et à stocker la table de correspondance des numéros de section, mais au lieu d'utiliser la méthode avec une complexité temporelle de O (n) ou O (log n), vous pouvez obtenir le résultat au prix de O (1). Mais j'ai décidé de suivre les conseils du grand Donald Knuth pour ne pas s'engager dans une optimisation prématurée sans besoin apparent et sans mesures appropriées. Oui, et pas à propos de cet article.

Et enfin, les fonctions qui acceptent les AdducedSectionTask et AdducedIndexPathTask indiquées ci-dessus et les «redirigent» vers des instances ComposedTableViewDataSource spécifiques:

 private func adduce<T>(_ section: SectionNumber, _ task: AdducedSectionTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: section) return task(dataSource, decomposedSection) } private func adduce<T>(_ indexPath: IndexPath, _ task: AdducedIndexPathTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: indexPath.section) return task(dataSource, IndexPath(row: indexPath.row, section: decomposedSection)) } 

Et maintenant, vous pouvez expliquer les noms que j'ai choisis pour toutes ces fonctions. C'est simple: ils reflètent un style de nommage fonctionnel. C'est-à-dire signifie littéralement peu, mais sonne impressionnant.

Les deux dernières fonctions ressemblent presque à des jumeaux, mais après un peu de réflexion, j'ai renoncé à me débarrasser de la duplication de code, car cela apportait plus d'inconvénients que d'avantages: je devais sortir ou transférer les fonctions de conversion vers le numéro de section et revenir au type d'origine. De plus, la probabilité de réutiliser cette approche généralisée tend à zéro.

Toutes ces préparations et aides donnent un avantage incroyable dans la mise en œuvre, en fait, des méthodes de protocole. Méthodes de configuration des tables:

 func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForHeaderInSection: $1) } } func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForFooterInSection: $1) } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return adduce(section) { $0.tableView(tableView, numberOfRowsInSection: $1) } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return adduce(indexPath) { $0.tableView(tableView, cellForRowAt: $1) } } 

Insérer et supprimer des lignes:

 func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { return adduce(indexPath) { $0.tableView?(tableView, commit: editingStyle, forRowAt: $1) } } func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { // Default if not implemented is "true". return adduce(indexPath) { $0.tableView?(tableView, canEditRowAt: $1) ?? true } } 

De la même manière, la prise en charge des en-têtes d'index de section peut être implémentée. Dans ce cas, au lieu du numéro de section, vous devez opérer avec l'index d'en-tête. En outre, il est très probable qu'il soit utile pour cela d'ajouter un champ supplémentaire au protocole ComposableTableViewDataSource . J'ai laissé cette pièce en dehors du matériau.

«L'impossible aujourd'hui sera possible demain»


Ce sont les mots du scientifique russe Konstantin Eduardovich Tsiolkovsky , le fondateur de la cosmonautique théorique.

Premièrement, la solution présentée ne prend pas en charge le glisser-déposer des lignes. Le plan d'origine incluait la prise en charge du glisser-déposer dans l'un des objets «source de données» constitutifs, mais, malheureusement, cela ne peut pas être réalisé en utilisant uniquement UITableViewDataSource . Les méthodes de ce protocole déterminent s'il est possible de «glisser-déposer» une ligne particulière et de recevoir un «rappel» à la fin du glisser-déposer. Et le traitement de l'événement lui-même est impliqué dans les méthodes UITableViewDelegate .

Deuxièmement, et plus important encore, il est nécessaire de réfléchir aux mécanismes de mise à jour des données à l'écran. Je pense que cela peut être implémenté en déclarant le protocole du délégué ComposableTableViewDataSource , dont les méthodes seront implémentées par ComposedTableViewDataSource et en recevant un signal que la "source de données" d'origine a reçu une mise à jour. Deux questions restent ouvertes: comment déterminer de manière fiable quel ComposableTableViewDataSource a changé à l'intérieur du ComposedTableViewDataSource et comment il s'agit d'une tâche distincte et non pas la plus triviale, mais ayant un certain nombre de solutions (par exemple, telles ). Et, bien sûr, vous aurez besoin du ComposedTableViewDataSource délégué ComposedTableViewDataSource , dont les méthodes seront appelées lors de la mise à jour de la "source de données" composite et implémentées par le type de client (par exemple, un contrôleur ou un modèle de vue ).

J'espère mieux étudier ces problèmes au fil du temps et les couvrir dans la deuxième partie de l'article. En attendant, je m'amuse, vous étiez curieux de lire sur ces expériences!

PS


L'autre jour, j'ai dû entrer dans le code mentionné dans l'introduction pour le modifier: j'avais besoin d'échanger les cellules de ces deux types. Bref, j'ai dû me tourmenter et «gagner ma vie» à Index out of bounds apparaissant constamment à différents endroits. Lors de l'utilisation de l'approche décrite, il ne serait nécessaire que d'échanger deux objets «source de données» dans le tableau passé comme argument d'initialisation.

Références:
- Playgroud avec code complet et exemple
- mon twitter

Source: https://habr.com/ru/post/fr442138/


All Articles