Reactive Data Display Manager. Einführung

Dies ist der erste Teil einer Reihe von Artikeln zur RDDM- Bibliothek (ReactiveDataDisplayManager) . In diesem Artikel werde ich die häufigsten Probleme beschreiben, mit denen ich mich bei der Arbeit mit „normalen“ Tabellen befassen muss, sowie eine Beschreibung von RDDM geben.




Problem 1. UITableViewDataSource


Vergessen Sie zunächst die Zuweisung von Verantwortlichkeiten, die Wiederverwendung und andere coole Wörter. Schauen wir uns die übliche Arbeit mit Tabellen an:

class ViewController: UIViewController { ... } extension ViewController: UITableViewDelegate { ... } extension ViewController: UITableViewDataSource { ... } 

Wir werden die häufigste Option analysieren. Was müssen wir implementieren? Richtig, UITableViewDataSource werden 3 UITableViewDataSource Methoden implementiert:

 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int func numberOfSections(in tableView: UITableView) -> Int func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) 

Im numberOfSection wir uns nicht mit numberOfSection ( numberOfSection usw.) numberOfSection und die interessanteste betrachten - func tableView(tableView: UITableView, indexPath: IndexPath)

Angenommen, wir möchten eine Tabelle mit Zellen mit einer Beschreibung der Produkte ausfüllen, dann sieht unsere Methode folgendermaßen aus:

 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 } 

Ausgezeichnet, es ist nicht schwierig. Angenommen, wir haben verschiedene Zelltypen, zum Beispiel drei:

  • Produkte
  • Liste der Aktien;
  • Werbung.

Zur Vereinfachung des Beispiels erhalten wir die Methode 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 } } 

Irgendwie viel Code. Stellen Sie sich vor, wir möchten den Einstellungsbildschirm erstellen. Was wird da sein?

  • Eine Zellenkappe mit einem Avatar;
  • Eine Reihe von Zellen mit Übergängen "in der Tiefe";
  • Zellen mit Schaltern (z. B. Eingabe über PIN-Code aktivieren / deaktivieren);
  • Zellen mit Informationen (z. B. eine Zelle, in der sich ein Telefon, eine E-Mail usw. befindet);
  • Persönliche Angebote.

Außerdem ist die Reihenfolge festgelegt. Eine großartige Methode wird sich herausstellen ...

Und jetzt noch eine Situation - es gibt ein Eingabeformular. Auf dem Eingabeformular eine Reihe identischer Zellen, von denen jede für ein bestimmtes Feld im Datenmodell verantwortlich ist. Beispielsweise ist die Zelle zum Eingeben des Telefons für das Telefon usw. verantwortlich.
Alles ist einfach, aber es gibt ein "ABER". In diesem Fall müssen Sie noch verschiedene Fälle malen, da Sie die erforderlichen Felder aktualisieren müssen.

Sie können weiterhin phantasieren und sich Backend Driven Design vorstellen, in dem wir 6 verschiedene Arten von Eingabefeldern erhalten. Je nach Status der Felder (Sichtbarkeit, Eingabetyp, Validierung, Standardwert usw.) ändern sich die Zellen so stark, dass sich ihre ändern kann nicht zu einer einzigen Schnittstelle führen. In diesem Fall sieht diese Methode sehr unangenehm aus. Auch wenn Sie die Konfiguration in verschiedene Methoden zerlegen.

Stellen Sie sich danach vor, wie Ihr Code aussehen wird, wenn Sie während der Arbeit Zellen hinzufügen / entfernen möchten. Es wird nicht sehr schön aussehen, da wir gezwungen sein werden, die Konsistenz der im ViewController gespeicherten ViewController und die Anzahl der Zellen unabhängig zu überwachen.

Die Probleme:

  • Wenn es Zellen unterschiedlichen Typs gibt, wird der Code nudelartig.
  • Es gibt viele Probleme bei der Behandlung von Ereignissen aus Zellen.
  • Hässlicher Code für den Fall, dass Sie den Status der Tabelle ändern müssen.

Problem 2. MindSet


Die Zeit für coole Worte ist noch nicht gekommen.
Schauen wir uns an, wie die Anwendung funktioniert oder wie die Daten auf dem Bildschirm angezeigt werden. Wir präsentieren diesen Prozess immer nacheinander. Nun, mehr oder weniger:

  1. Daten aus dem Netzwerk abrufen;
  2. Zu verarbeiten;
  3. Zeigen Sie diese Daten auf dem Bildschirm an.

Aber ist es wirklich so? Nein! Tatsächlich machen wir das:

  1. Daten aus dem Netzwerk abrufen;
  2. Zu verarbeiten;
  3. Im ViewController-Modell speichern;
  4. Etwas bewirkt eine Bildschirmaktualisierung.
  5. Das gespeicherte Modell wird in Zellen konvertiert.
  6. Daten werden auf dem Bildschirm angezeigt.

Neben der Menge gibt es noch Unterschiede. Erstens geben wir keine Daten mehr aus, sondern werden ausgegeben. Zweitens gibt es eine logische Lücke im Datenverarbeitungsprozess, das Modell wird gespeichert und der Prozess endet dort. Dann passiert etwas und ein anderer Prozess beginnt. Daher fügen wir dem Bildschirm offensichtlich keine Elemente hinzu, sondern speichern sie (die übrigens auch voll sind) nur bei Bedarf.

UITableViewDelegate Sie auch an UITableViewDelegate . Es enthält auch Methoden zum Bestimmen der UITableViewDelegate . Normalerweise reicht die automaticDimension Abmessung aus, aber manchmal reicht dies nicht aus, und Sie müssen die Höhe selbst einstellen (z. B. bei Animationen oder für Überschriften).
Dann teilen wir im Allgemeinen die Zelleneinstellungen, das Teil mit der Höhenkonfiguration ist in einer anderen Methode.

Die Probleme:

  • Die explizite Verbindung zwischen der Datenverarbeitung und ihrer Anzeige auf der Benutzeroberfläche geht verloren.
  • Die Zellenkonfiguration gliedert sich in verschiedene Teile.

Idee


Die aufgeführten Probleme auf komplexen Bildschirmen verursachen Kopfschmerzen und ein scharfes Verlangen nach Tee.

Erstens möchte ich nicht ständig Delegierungsmethoden implementieren. Die naheliegende Lösung besteht darin, ein Objekt zu erstellen, das es implementiert. Als nächstes machen wir so etwas wie:

 let displayManager = DisplayManager(self.tableView) 

Großartig. Jetzt muss das Objekt mit beliebigen Zellen arbeiten können, während die Konfiguration dieser Zellen an einen anderen Ort verschoben werden muss.

Wenn wir die Konfiguration in ein separates Objekt einfügen, kapseln wir die Konfiguration an einem Ort (es ist Zeit für intelligente Wörter). An derselben Stelle können wir die Logik zum Formatieren von Daten herausnehmen (z. B. Ändern des Datumsformats, Verketten von Zeichenfolgen usw.). Über dasselbe Objekt können wir Ereignisse in der Zelle abonnieren.

In diesem Fall haben wir ein Objekt mit zwei verschiedenen Schnittstellen:

  1. Die Schnittstelle zur Generierung von UITableView Instanzen ist für unseren DisplayManager vorgesehen.
  2. Initialisierungs-, Abonnement- und Konfigurationsoberfläche - für Presenter oder ViewController.

Wir nennen dieses Objekt einen Generator. Dann ist unser Generator für die Tabelle eine Zelle und für alles andere eine Möglichkeit, Daten auf einer Benutzeroberfläche darzustellen und Ereignisse zu verarbeiten.

Und da die Konfiguration jetzt vom Generator gekapselt wird und der Generator selbst eine Zelle ist, können wir viele Probleme lösen. Einschließlich der oben aufgeführten.

Implementierung


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

Mit solchen Implementierungen können wir die Standardimplementierung vornehmen:

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

Ich werde ein Beispiel für einen kleinen Generator geben:

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

Hier haben wir sowohl die Konfiguration als auch die Abonnements versteckt. Bitte beachten Sie, dass wir jetzt einen Ort haben, an dem wir den Status kapseln können (da es unmöglich ist, den Status in der Zelle zu kapseln, da er von der Tabelle wiederverwendet wird). Außerdem hatten sie die Möglichkeit, die Daten in der Zelle "on the fly" zu ändern.

self.cell = view auf self.cell = view . Wir haben uns an die Zelle erinnert und können jetzt die Daten aktualisieren, ohne diese Zelle neu zu laden. Dies ist eine nützliche Funktion.

Aber ich war abgelenkt. Da jede Zelle durch einen Generator dargestellt werden kann, können wir die Benutzeroberfläche unseres DisplayManager etwas schöner gestalten.

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

Das ist eigentlich nicht alles. Wir können Generatoren an den richtigen Stellen einfügen oder löschen.

Übrigens kann das Einfügen einer Zelle nach einer bestimmten Zelle verdammt nützlich sein. Insbesondere wenn wir die Daten nach und nach laden (z. B. hat der Benutzer die TIN eingegeben, haben wir die TIN-Informationen hochgeladen und angezeigt, indem wir nach dem TIN-Feld mehrere neue Zellen hinzugefügt haben).

Zusammenfassung


Wie die Zellarbeit jetzt aussehen wird:

 class ViewController: UIViewController { func update(data: [Products]) { let gens = data.map { ProductCellGenerator($0) } self.ddm.addGenerators(gens) } } 

Oder hier:

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

Wir können die Reihenfolge des Hinzufügens von Elementen steuern und gleichzeitig geht die Verbindung zwischen der Datenverarbeitung und dem Hinzufügen zur Benutzeroberfläche nicht verloren. In einfachen Fällen haben wir also einfachen Code. In schwierigen Fällen verwandelt sich der Code nicht in Pasta und sieht gleichzeitig passabel aus. Eine deklarative Schnittstelle für die Arbeit mit Tabellen wurde angezeigt, und jetzt kapseln wir die Konfiguration von Zellen, wodurch wir Zellen zusammen mit Konfigurationen zwischen verschiedenen Bildschirmen wiederverwenden können.

Vorteile der Verwendung von RDDM:

  • Kapselung der Zellkonfiguration;
  • Reduzieren der Codeduplizierung durch Einkapseln von Arbeiten aus Sammlungen in den Adapter;
  • Wählen Sie ein Adapterobjekt aus, das die spezifische Logik der Arbeit mit Sammlungen kapselt.
  • Der Code wird offensichtlicher und leichter zu lesen.
  • Die Menge an Code, die geschrieben werden muss, um eine Tabelle hinzuzufügen, wird reduziert.
  • Der Prozess der Verarbeitung von Ereignissen aus Zellen wird vereinfacht.

Quellen hier .

Vielen Dank für Ihre Aufmerksamkeit!

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


All Articles