Wir alle müssen uns oft mit statischen Tabellen befassen. Dies können die Einstellungen unserer Anwendung, Autorisierungsbildschirme, "Über uns" -Bildschirme und viele andere sein. Aber oft wenden unerfahrene Entwickler keine Entwicklungsmuster für solche Tabellen an und schreiben alle in einer Klasse ein nicht skalierbares, unflexibles System.
Wie ich dieses Problem löse - unter dem Schnitt.
Worüber sprichst du?
Bevor Sie das Problem der statischen Tabellen lösen, sollten Sie verstehen, was es ist. Statische Tabellen sind Tabellen, in denen Sie bereits die Anzahl der Zeilen und den darin enthaltenen Inhalt kennen. Beispiele für ähnliche Tabellen unten.

Das Problem
Zunächst lohnt es sich, das Problem zu identifizieren: Warum können wir nicht einfach einen ViewController erstellen, der UITableViewDelegate und UITableViewDatasource ist, und einfach alle benötigten Zellen beschreiben? Zumindest - es gibt 5 Probleme mit unserer Tabelle:
- Schwer zu skalieren
- Indexabhängig
- Nicht flexibel
- Fehlende Wiederverwendung
- Benötigt viel Code zum Initialisieren
Lösung
Die Methode zur Lösung des Problems basiert auf der folgenden Grundlage:
- Aufhebung der Verantwortung für die Konfiguration der Tabelle in einer separaten Klasse ( Konstruktor )
- Benutzerdefinierter Wrapper über UITableViewDelegate und UITableViewDataSource
- Verbinden von Zellen mit benutzerdefinierten Protokollen zur Wiederverwendung
- Erstellen Sie Ihre eigenen Datenmodelle für jede Tabelle
Zuerst möchte ich zeigen, wie dies in der Praxis angewendet wird - dann werde ich zeigen, wie alles unter der Haube implementiert wird.
Implementierung
Die Aufgabe besteht darin, eine Tabelle mit zwei Textzellen und einer leeren dazwischen zu erstellen.
Zunächst habe ich mit
UILabel eine reguläre
TextTableViewCell erstellt .
Als nächstes benötigt jeder UIViewController mit einer statischen Tabelle einen eigenen Konstruktor. Erstellen wir ihn:
class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = <#type#> }
Wenn wir es vom
StaticConstructorContainer geerbt haben , müssen wir nach dem generischen Protokoll zunächst das Modell (
ModelType )
eingeben - dies ist der Typ des Zellmodells, den wir ebenfalls erstellen müssen.
Ich benutze dafür enum, da es besser für unsere Aufgaben geeignet ist und hier der Spaß beginnt. Wir füllen unsere Tabelle mit Inhalten unter Verwendung von Protokollen wie:
Titel, Untertitel, Farbig, Schrift und so weiter. Wie Sie sich vorstellen können, sind diese Protokolle für die Anzeige von Text verantwortlich. Angenommen, das Titelprotokoll erfordert den
Titel: String? Wenn unsere Zelle Titelanzeigen unterstützt, wird sie gefüllt. Mal sehen, wie es aussieht:
protocol Fonted { var font: UIFont? { get } } protocol FontedConfigurable { func configure(by model: Fonted) } protocol Titled { var title: String? { get } } protocol TitledConfigurable { func configure(by model: Titled) } protocol Subtitled { var subtitle: String? { get } } protocol SubtitledConfigurable { func configure(by model: Subtitled) } protocol Imaged { var image: UIImage? { get } } protocol ImagedConfigurable { func configure(by model: Imaged) }
Dementsprechend wird hier nur ein kleiner Teil solcher Protokolle vorgestellt. Sie können sie selbst erstellen, wie Sie sehen - es ist sehr einfach. Ich erinnere Sie daran, dass wir sie 1 Mal für einen Zweck erstellen und sie dann vergessen und ruhig verwenden.
Unsere Zelle (
mit Text ) unterstützt im Wesentlichen die folgenden Dinge: Die Schriftart des Textes, den Text selbst, die Farbe des Textes, die Hintergrundfarbe der Zelle und im Allgemeinen alle Dinge, die Ihnen in den Sinn kommen.
Wir brauchen bisher nur den
Titel . Daher erben wir unser Modell von Titled. Im Modell geben wir für den Fall an, welche Zelltypen wir haben werden.
enum CellModel: Titled { case firstText case emptyMiddle case secondText var title: String? { switch self { case .firstText: return " - " case .secondText: return " - " default: return nil } } }
Da sich in der Mitte keine Beschriftung befindet (leere Zelle), können Sie null zurückgeben.
Wir haben die C-Zelle fertiggestellt und Sie können sie in unseren Konstruktor einfügen.
class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = CellModel var models: [CellModel]
Und in der Tat ist dies unser ganzer Code. Wir können sagen, dass unser Tisch fertig ist. Lassen Sie uns die Daten ausfüllen und sehen, was passiert.
Oh ja, ich hätte es fast vergessen. Wir müssen unsere Zelle vom TitledConfigurable-Protokoll erben, damit sie einen Titel in sich selbst einfügen kann. Zellen unterstützen auch dynamische Höhen.
extension TextTableViewCell: TitledConfigurable { func configure(by model: Titled) { label.text = model.title } }
Wie der gefüllte Konstruktor aussieht:
class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = CellModel var models: [CellModel] = [.firstText, .emptyMiddle, .secondText] func cellType(for model: CellModel) -> StaticTableViewCellClass.Type { switch model { case .emptyMiddle: return EmptyTableViewCell.self case .firstText, .secondText: return TextTableViewCell.self } } func configure(cell: UITableViewCell, by model: CellModel) { cell.selectionStyle = .none } func itemSelected(item: CellModel) { switch item { case .emptyMiddle: print(" ") default: print(" ...") } } }
Sieht ziemlich kompakt aus, oder?
Das Letzte, was wir noch tun müssen, ist, alles mit dem ViewController zu verbinden:
class ViewController: UIViewController { private let tableView: UITableView = { let tableView = UITableView() return tableView }() private let constructor = ViewControllerConstructor() private lazy var delegateDataSource = constructor.delegateDataSource() override func viewDidLoad() { super.viewDidLoad() constructor.setup(at: tableView, dataSource: delegateDataSource) } }
Alles ist fertig, wir müssen
delegateDataSource als separate Eigenschaft in unserer Klasse festlegen, damit das schwache Glied in keiner Funktion unterbrochen wird.
Wir können laufen und testen:

Wie Sie sehen können, funktioniert alles.
Lassen Sie uns nun zusammenfassen und verstehen, was wir erreicht haben:
- Wenn wir eine neue Zelle erstellen und die aktuelle durch diese ersetzen möchten, ändern wir dazu eine Variable. Wir haben ein sehr flexibles Tischsystem
- Wir verwenden alle Zellen wieder. Je mehr Zellen Sie mit dieser Tabelle verknüpfen, desto einfacher ist es, damit zu arbeiten. Ideal für große Projekte.
- Wir haben die Menge an Code zum Erstellen der Tabelle reduziert. Und wir müssen es noch weniger schreiben, wenn wir viele Protokolle und statische Zellen im Projekt haben.
- Wir haben die Erstellung statischer Tabellen vom UIViewController zum Konstruktor gebracht
- Wir sind nicht mehr von Indizes abhängig, wir können die Zellen im Array sicher austauschen und die Logik wird nicht kaputt gehen.
Code für ein Testprojekt am Ende des Artikels.
Wie funktioniert es von innen nach außen?
Wie die Protokolle funktionieren, haben wir bereits besprochen. Jetzt müssen wir verstehen, wie der gesamte Konstruktor und die zugehörigen Klassen funktionieren.
Beginnen wir mit dem Konstruktor selbst:
protocol StaticConstructorContainer { associatedtype ModelType var models: [ModelType] { get } func cellType(for model: ModelType) -> StaticTableViewCellClass.Type func configure(cell: UITableViewCell, by model: ModelType) func itemSelected(item: ModelType) }
Dies ist ein gängiges Protokoll, das Funktionen erfordert, die uns bereits bekannt sind.
Interessanter ist seine
Erweiterung :
extension StaticConstructorContainer { typealias StaticTableViewCellClass = StaticCell & NibLoadable func delegateDataSource() -> StaticDataSourceDelegate<Self> { return StaticDataSourceDelegate<Self>.init(container: self) } func setup<T: StaticConstructorContainer>(at tableView: UITableView, dataSource: StaticDataSourceDelegate<T>) { models.forEach { (model) in let type = cellType(for: model) tableView.register(type.nib, forCellReuseIdentifier: type.name) } tableView.delegate = dataSource tableView.dataSource = dataSource dataSource.tableView = tableView } }
Die
Setup- Funktion, die wir in unserem ViewController aufgerufen haben, registriert alle Zellen für uns und delegiert
dataSource und
delegate .
Und
delegateDataSource () erstellt für uns einen Wrapper
UITableViewDataSource und
UITableViewDelegate . Schauen wir es uns an:
class StaticDataSourceDelegate<Container: StaticConstructorContainer>: NSObject, UITableViewDelegate, UITableViewDataSource { private let container: Container weak var tableView: UITableView? init(container: Container) { self.container = container } func reload() { tableView?.reloadData() } func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { let type = container.cellType(for: container.models[indexPath.row]) return type.estimatedHeight ?? type.height } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let type = container.cellType(for: container.models[indexPath.row]) return type.height } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return container.models.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let model = container.models[indexPath.row] let type = container.cellType(for: model) let cell = tableView.dequeueReusableCell(withIdentifier: type.name, for: indexPath) if let typedCell = cell as? TitledConfigurable, let titled = model as? Titled { typedCell.configure(by: titled) } if let typedCell = cell as? SubtitledConfigurable, let subtitle = model as? Subtitled { typedCell.configure(by: subtitle) } if let typedCell = cell as? ImagedConfigurable, let imaged = model as? Imaged { typedCell.configure(by: imaged) } container.configure(cell: cell, by: model) return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let model = container.models[indexPath.row] container.itemSelected(item: model) } }
Ich denke, es gibt keine Fragen zu den Funktionen
heightForRowAt ,
numberOfRowsInSection ,
didSelectRowAt , sie implementieren nur klare Funktionen. Die interessanteste Methode ist hier
cellForRowAt .
Darin implementieren wir nicht die schönste Logik. Wir sind gezwungen, jedes neue Protokoll in die Zellen hier zu schreiben, aber wir machen es einmal - es ist also nicht so beängstigend. Wenn das Modell genau wie unsere Zelle dem Protokoll entspricht, werden wir es konfigurieren. Wenn jemand Ideen hat, wie dies automatisiert werden kann, werde ich gerne in den Kommentaren zuhören.
Damit ist die Logik beendet. Ich habe in diesem System keine utilitaristischen Klassen von Drittanbietern angesprochen.
Den vollständigen Code finden Sie hier .
Vielen Dank für Ihre Aufmerksamkeit!