Zusammengesetztes "Datenquellen" -Objekt und Elemente eines funktionalen Ansatzes

Einmal stand ich ( UICollectionView , nicht einmal ich) vor der Aufgabe, der UICollectionView eine Zelle eines völlig anderen Typs mit einem bestimmten Zelltyp hinzuzufügen, und dies nur in einem speziellen Fall, der „oben“ verarbeitet wird und nicht direkt von UICollectionView . Diese Aufgabe führte, wenn mein Gedächtnis mir dient, zu ein paar hässlichen if else Blöcken innerhalb der UICollectionViewDelegate UICollectionViewDataSource und UICollectionViewDelegate , die sich sicher im "Produktions" -Code niederließen und wahrscheinlich von dort aus nirgendwo hingehen werden.

Im Rahmen der oben genannten Aufgabe war es sinnlos, über eine elegantere Lösung nachzudenken oder „nachdenkliche“ Energie dafür zu verschwenden. Trotzdem erinnerte ich mich an diese Geschichte: Ich dachte darüber nach, ein bestimmtes "Datenquellen" -Objekt zu implementieren, das aus einer beliebigen Anzahl anderer "Datenquellen" -Objekte zu einem Ganzen zusammengesetzt werden könnte. Die Lösung sollte natürlich verallgemeinert sein, für eine beliebige Anzahl von Komponenten (einschließlich Null und Eins) geeignet sein und nicht von bestimmten Typen abhängen. Es stellte sich heraus, dass dies nicht nur real, sondern auch nicht zu schwierig ist (obwohl es etwas schwieriger ist, den Code auch „schön“ zu machen).

Ich werde zeigen, was ich mit dem UITableView Beispiel gemacht habe. Wenn Sie möchten, sollte es nicht schwierig sein, einen ähnlichen Code für die UICollectionView schreiben.

„Eine Idee ist immer wichtiger als ihre Verkörperung“


Dieser Aphorismus gehört dem großen Comicautor Alan Moore ( "Keepers ", "V steht für Vendetta ", "League of Outstanding Gentlemen" ), aber es geht nicht wirklich um Programmierung, oder?

Die Hauptidee meines Ansatzes besteht darin, ein Array von UITableViewDataSource Objekten zu speichern, die Gesamtzahl der Abschnitte zurückzugeben und beim Zugriff auf den Abschnitt zu bestimmen, welches der ursprünglichen "Datenquellen" -Objekte diesen Aufruf umleiten wird.

Das UITableViewDataSource Protokoll verfügt bereits über die erforderlichen Methoden zum UITableViewDataSource der Anzahl von Abschnitten, Zeilen usw., aber leider war es in diesem Fall äußerst unpraktisch, es zu verwenden, da ein Verweis auf eine bestimmte Instanz von UITableView als eines der Argumente übergeben werden muss. Aus diesem UITableViewDataSource ich beschlossen, das Standardprotokoll UITableViewDataSource einige zusätzliche einfache UITableViewDataSource zu erweitern:

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

Die zusammengesetzte „Datenquelle“ stellte sich als einfache Klasse heraus, die die Anforderungen von UITableViewDataSource und mit nur einem Argument initialisiert wird - einer Reihe spezifischer Instanzen von 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.") } } 

Jetzt müssen nur noch die Implementierungen aller Methoden des UITableViewDataSource Protokolls so geschrieben werden, dass sie auf die Methoden der entsprechenden Komponenten verweisen.

„Es war die richtige Entscheidung. Meine Entscheidung


Diese Worte gehörten Boris Nikolayevich Jelzin , dem ersten Präsidenten der Russischen Föderation , und beziehen sich nicht wirklich auf den folgenden Text, ich mochte sie einfach.

Die richtige Entscheidung schien mir, die Funktionalität der Swift- Sprache zu nutzen, und es stellte sich wirklich als praktisch heraus.

Zunächst implementieren wir eine Methode, die die Anzahl der Abschnitte zurückgibt - dies ist nicht schwierig. Wie oben erwähnt, benötigen wir nur die Gesamtzahl aller Abschnitte der Komponenten:

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

(Ich werde die Syntax und Bedeutung von Standardfunktionen nicht erklären. Bei Bedarf ist das Internet voll von guten Einführungsartikeln zu diesem Thema . Und ich kann auch ein ziemlich gutes Buch empfehlen.)

Bei einem kurzen Blick auf alle Methoden von UITableViewDataSource werden Sie feststellen, dass sie als Argumente nur einen Link zur Tabelle und den Wert der Abschnittsnummer oder der entsprechenden IndexPath Zeile IndexPath . Wir werden einige Helfer schreiben, die uns bei der Implementierung aller anderen Protokollmethoden hilfreich sein werden.

Erstens können alle Aufgaben auf eine "generische" Funktion reduziert werden, die als Argumente einen Verweis auf eine bestimmte ComposableTableViewDataSource und den Wert der Abschnittsnummer oder des IndexPath . Der Einfachheit halber weisen wir den Typen dieser Funktionen Pseudonyme zu . Zur besseren Lesbarkeit empfehle ich außerdem, einen Alias ​​für die Abschnittsnummer anzugeben:

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

(Ich werde die ausgewählten Namen gleich unten erklären.)

Zweitens implementieren wir eine einfache Funktion, die die spezifische ComposableTableViewDataSource und die entsprechende Abschnittsnummer anhand der ComposedTableViewDataSource Abschnittsnummer bestimmt:

 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) } 

Wenn Sie etwas länger als meine denken, wird sich die Implementierung möglicherweise als eleganter und weniger einfach herausstellen. Zum Beispiel schlugen meine Kollegen sofort vor, dass ich in dieser Funktion eine binäre Suche implementiere (zuvor beispielsweise während der Initialisierung, indem ich den Index der Anzahl der Abschnitte zusammensetze - ein einfaches Array von Ganzzahlen ). Oder verbringen Sie ein wenig Zeit mit dem Kompilieren und Speichern der Korrespondenztabelle mit Abschnittsnummern. Statt jedoch die Methode mit der zeitlichen Komplexität von O (n) oder O (log n) zu verwenden, können Sie das Ergebnis auf Kosten von O (1) erhalten. Aber ich beschloss, den Rat des großen Donald Knuth zu befolgen, um keine vorzeitige Optimierung ohne offensichtliche Notwendigkeit und geeignete Messungen vorzunehmen. Ja und nicht zu diesem Artikel.

Und schließlich Funktionen, die die AdducedSectionTask angegebenen AdducedSectionTask und AdducedIndexPathTask Funktionen akzeptieren und sie an bestimmte ComposedTableViewDataSource Instanzen AdducedIndexPathTask :

 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)) } 

Und jetzt können Sie die Namen erklären, die ich für alle diese Funktionen gewählt habe. Es ist ganz einfach: Sie spiegeln einen funktionalen Namensstil wider. Das heißt, bedeuten buchstäblich wenig, klingen aber beeindruckend.

Die letzten beiden Funktionen sehen fast wie Zwillinge aus, aber nach einigem Nachdenken gab ich es auf, die Codeduplizierung loszuwerden, da dies mehr Unannehmlichkeiten als Vorteile mit sich brachte: Ich musste die Konvertierungsfunktionen auf die Abschnittsnummer und zurück zum ursprünglichen Typ ausgeben oder übertragen. Darüber hinaus tendiert die Wahrscheinlichkeit, diesen verallgemeinerten Ansatz wiederzuverwenden, gegen Null.

All diese Vorbereitungen und Helfer bieten einen unglaublichen Vorteil bei der Implementierung der Protokollmethoden. Tabellenkonfigurationsmethoden:

 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) } } 

Zeilen einfügen und löschen:

 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 } } 

In ähnlicher Weise kann die Unterstützung für Abschnittsindex-Header implementiert werden. In diesem Fall müssen Sie anstelle der Abschnittsnummer mit dem Header-Index arbeiten. Außerdem ist es höchstwahrscheinlich hilfreich, dem ComposableTableViewDataSource Protokoll ein zusätzliches Feld hinzuzufügen. Ich habe dieses Stück außerhalb des Materials gelassen.

"Das Unmögliche heute wird morgen möglich sein"


Dies sind die Worte des russischen Wissenschaftlers Konstantin Eduardovich Tsiolkovsky , dem Begründer der theoretischen Kosmonautik.

Erstens unterstützt die vorgestellte Lösung das Ziehen und Ablegen von Zeilen nicht. Der ursprüngliche Plan enthielt die Unterstützung für Drag & Drop innerhalb eines der konstituierenden "Datenquellen" -Objekte. Dies kann jedoch leider nicht nur mit UITableViewDataSource erreicht werden. Die Methoden dieses Protokolls bestimmen, ob es möglich ist, eine bestimmte Zeile zu ziehen und abzulegen und am Ende des Ziehens einen Rückruf zu erhalten. Die Verarbeitung des Ereignisses selbst ist in den UITableViewDelegate Methoden enthalten.

Zweitens und noch wichtiger ist es, über Mechanismen zur Aktualisierung von Daten auf dem Bildschirm nachzudenken. Ich denke, dies kann implementiert werden, indem das ComposableTableViewDataSource Delegatenprotokoll deklariert wird, dessen Methoden von ComposedTableViewDataSource implementiert werden, und indem ein Signal empfangen wird, dass die ursprüngliche "Datenquelle" ein Update erhalten hat. Zwei Fragen bleiben offen: Wie kann zuverlässig festgestellt werden, welche ComposableTableViewDataSource sich in der ComposedTableViewDataSource geändert hat, und wie es sich um eine separate und nicht trivialste Aufgabe handelt, die jedoch eine Reihe von Lösungen aufweist (z. B. solche ). Und natürlich benötigen Sie das ComposedTableViewDataSource Delegatenprotokoll, dessen Methoden beim Aktualisieren der zusammengesetzten "Datenquelle" aufgerufen und vom Clienttyp (z. B. einem Controller oder einem Ansichtsmodell ) implementiert werden.

Ich hoffe, diese Probleme im Laufe der Zeit besser untersuchen und im zweiten Teil des Artikels behandeln zu können. In der Zwischenzeit, amüsiere ich mich, waren Sie neugierig, über diese Experimente zu lesen!

PS


Erst neulich musste ich in den in der Einleitung erwähnten Code einsteigen, um ihn zu ändern: Ich musste die Zellen dieser beiden Typen austauschen. Kurz gesagt, ich musste mich selbst quälen und von Index out of bounds „leben“ Index out of bounds ständig an verschiedenen Orten auftaucht. Bei Verwendung des beschriebenen Ansatzes müssten nur die beiden als Initialisierungsargument übergebenen "Datenquellen" -Objekte in dem Array ausgetauscht werden.

Referenzen:
- Playgroud mit vollständigem Code und Beispiel
- mein Twitter

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


All Articles