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 {
(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 {
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