Zerlegen einer UICollectionViewCell

Nachdem ich Keynote WWDC 2019 gesehen und SwiftUI kennengelernt habe , mit dem Benutzeroberflächen deklarativ im Code beschrieben werden sollen, möchte ich darüber spekulieren, wie Platten und Sammlungen deklarativ ausgefüllt werden können. Zum Beispiel so:


enum Builder { static func widgets(objects: Objects) -> [Widget] { let header = [ Spacing(height: 25).widget, Header(string: " ").widget, Spacing(height: 10, separator: .bottom).widget ] let body = objects .flatMap({ (object: Object) -> [Widgets] in return [ Title(object: object).widget, Spacing(height: 1, separator: .bottom).widget ] }) return header + body } } let objects: [Object] = ... Builder .widgets(objects: objects) .bind(to: collectionView) 

In der Sammlung wird dies wie folgt gerendert:

Bild


Einführung


Wie Sie aus maßgeblichen Quellen wissen: Die meiste Zeit verbringt ein typischer iOS-Entwickler die Arbeit mit Tablets. Wenn wir davon ausgehen, dass die Entwickler des Projekts sehr fehlen und die Tablets nicht einfach sind, bleibt keine Zeit für den Rest der Anwendung. Und damit muss etwas getan werden ... Eine mögliche Lösung wäre die Zellzersetzung.


Zellzerlegung bedeutet, eine Zelle durch mehrere kleinere Zellen zu ersetzen. Mit diesem Ersatz sollte sich optisch nichts ändern. Betrachten Sie als Beispiel Beiträge aus dem VK-Newsfeed für iOS. Ein Beitrag kann sowohl als einzelne Zelle als auch als Gruppe von Zellen - Primitiven - dargestellt werden.


Das Zerlegen von Zellen funktioniert nicht immer. Es wird schwierig sein, eine Zelle in Grundelemente zu zerlegen, die auf allen Seiten einen Schatten oder eine Rundung aufweisen. In diesem Fall ist die ursprüngliche Zelle ein Grundelement.


Vorteile und Nachteile


Durch die Zerlegung von Zellen bestehen Tabellen / Sammlungen aus Grundelementen, die häufig wiederverwendet werden: einem Grundelement mit Text, einem Grundelement mit Bild, einem Grundelement mit Hintergrund usw. Die Berechnung der Höhe eines einzelnen Grundelements ist viel einfacher und effektiver als eine komplexe Zelle mit einer großen Anzahl von Zuständen. Falls gewünscht, kann die dynamische Höhe des CTFramesetter berechnet oder sogar im Hintergrund gezeichnet werden (z. B. Text über CTFramesetter ).


Andererseits wird das Arbeiten mit Daten komplizierter. Für jedes IndexPath Daten benötigt, und gemäß dem IndexPath es schwierig zu bestimmen, zu welcher realen Zelle es gehört. Wir müssen neue Abstraktionsebenen einführen oder diese Probleme irgendwie lösen.


Sie können lange über die möglichen Vor- und Nachteile dieses Unternehmens sprechen, aber es ist besser, den Ansatz zur Zellzersetzung zu beschreiben.


Werkzeuge auswählen


Da UITableView in seinen Funktionen begrenzt ist und wir, wie bereits erwähnt, ziemlich komplexe Platten haben, wäre die Verwendung von UICollectionView eine geeignete Lösung. Es geht um UICollectionView , das in dieser Veröffentlichung behandelt wird.


Bei Verwendung von UICollectionView mit einer Situation konfrontiert, in der das Basis- UICollectionViewFlowLayout nicht die gewünschte Anordnung der Elemente der Sammlung bilden kann (das neue UICollectionViewCompositionalLayout nicht berücksichtigt). In solchen Momenten wird normalerweise die Entscheidung getroffen, ein Open-Source- UICollectionViewLayout . Aber selbst unter vorgefertigten Lösungen gibt es möglicherweise keine geeignete, wie zum Beispiel auf der dynamischen Hauptseite eines großen Online-Shops oder eines sozialen Netzwerks. Wir gehen vom Schlimmsten aus und erstellen unser eigenes universelles UICollectionViewLayout .


Zusätzlich zu den Schwierigkeiten bei der Auswahl eines Layouts müssen Sie entscheiden, wie die Sammlung Daten empfangen soll. Zusätzlich zu dem üblichen Ansatz, bei dem ein Objekt (meistens ein UIViewController ) dem UICollectionViewDataSource Protokoll entspricht und Daten für eine Sammlung bereitstellt, wird die Verwendung datengesteuerter Frameworks immer beliebter. Helle Vertreter dieses Ansatzes sind CollectionKit , IGListKit , RxDataSources und andere. Die Verwendung solcher Frameworks vereinfacht die Arbeit mit Sammlungen und bietet die Möglichkeit, Datenänderungen zu animieren, weil Der unterschiedliche Algorithmus ist bereits im Framework vorhanden. Für Veröffentlichungszwecke wird das RxDataSources- Framework ausgewählt.


Widget und seine Eigenschaften


Wir führen eine Zwischendatenstruktur ein und nennen sie ein Widget . Wir beschreiben die Haupteigenschaften, die ein Widget haben sollte:


  1. Das Widget muss den erforderlichen Protokollen für die Verwendung des datengesteuerten Frameworks entsprechen. Solche Protokolle enthalten typischerweise einen zugeordneten Wert (z. B. IdentifiableType Typ in RxDataSources ).
  2. Es sollte möglich sein, Widgets für verschiedene Grundelemente zu einem Array zusammenzufassen. Um dies zu erreichen, sollte dem Widget keine Werte zugeordnet sein. Für diese Zwecke können Sie den Mechanismus der Typlöschung oder ähnliches verwenden.
  3. Das Widget sollte in der Lage sein, die Größe des Grundelements zu zählen. Beim Bilden des UICollectionViewLayout müssen dann nur noch die UICollectionViewLayout gemäß den vordefinierten Regeln korrekt positioniert werden.
  4. Das Widget muss die Factory für die UICollectionViewCell . Daher wird bei der Implementierung von UICollectionViewDataSource gesamte Logik zum Erstellen von Zellen entfernt und alles, was übrig bleibt, ist:
     let cell = widget.widgetCell(collectionView: collectionView, indexPath: indexPath) return cell 

Widget-Implementierung


Um das Widget mit dem RxDataSources- Framework verwenden zu können, muss es den Protokollen Equatable und IdentifiableType . Da das Widget ein Grundelement darstellt, ist es für Veröffentlichungszwecke ausreichend, wenn sich das Widget identifiziert, um dem IdentifiableType Protokoll zu entsprechen. In der Praxis wirkt sich dies auf die Tatsache aus, dass das Primitiv beim Ändern des Widgets nicht neu geladen, sondern gelöscht und eingefügt wird. WidgetIdentifiable Sie dazu das neue WidgetIdentifiable Protokoll ein:


 protocol WidgetIdentifiable: IdentifiableType { } extension WidgetIdentifiable { var identity: Self { return self } } 

Um dem WidgetIdentifiable zu entsprechen, WidgetIdentifiable das Widget dem Hashable Protokoll entsprechen. Hashable Widget übernimmt Daten zur Einhaltung des Protokolls von dem Objekt, das ein bestimmtes Hashable beschreibt. Mit AnyHashable können Sie das Objekttyp-Widget "löschen".


 struct Widget: WidgetIdentifiable { let underlying: AnyHashable init(_ underlying: AnyHashable) { self.underlying = underlying } } extension Widget: Hashable { func hash(into hasher: inout Hasher) { self.underlying.hash(into: &hasher) } static func ==(lhs: Widget, rhs: Widget) -> Bool { return lhs.underlying == rhs.underlying } } 

In dieser Phase werden die ersten beiden Eigenschaften des Widgets ausgeführt. Dies ist nicht schwer zu überprüfen, indem mehrere Widgets mit unterschiedlichen Objekttypen in einem Array gesammelt werden.


 let widgets = [Widget("Hello world"), Widget(100500)] 

Um die verbleibenden Eigenschaften zu implementieren, führen wir ein neues Protokoll WidgetPresentable


 protocol WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell func widgetSize(containerWidth: CGFloat) -> CGSize } 

Die Funktion widgetSize(containerWidth:) wird im UICollectionViewLayout beim Generieren der Attribute der Zellen und widgetCell(collectionView:indexPath:) , um die Zellen widgetCell(collectionView:indexPath:) .


Wenn das Widget WidgetPresentable Protokoll WidgetPresentable das Widget alle zu Beginn der Veröffentlichung angegebenen Eigenschaften. Das im Widget AnyHashable enthaltene Objekt muss jedoch durch die Komposition WidgetPresentable und WidgetHashable , wobei WidgetHashable keinen zugeordneten Wert hat (wie im Fall von Hashable ) und der Typ des Objekts im Widget "gelöscht" bleibt:


 protocol WidgetHashable { func widgetEqual(_ any: Any) -> Bool func widgetHash(into hasher: inout Hasher) } 

In der endgültigen Version sieht das Widget folgendermaßen aus:


 struct Widget: WidgetIdentifiable { let underlying: WidgetHashable & WidgetPresentable init(_ underlying: WidgetHashable & WidgetPresentable) { self.underlying = underlying } } extension Widget: Hashable { func hash(into hasher: inout Hasher) { self.underlying.widgetHash(into: &hasher) } static func ==(lhs: Widget, rhs: Widget) -> Bool { return lhs.underlying.widgetEqual(rhs.underlying) } } extension Widget: WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return underlying.widgetCell(collectionView: collectionView, indexPath: indexPath) } func widgetSize(containerWidth: CGFloat) -> CGSize { return underlying.widgetSize(containerWidth: containerWidth) } } 

Primitives Objekt


Versuchen wir, das einfachste Grundelement zusammenzusetzen, das die Einrückung einer bestimmten Höhe ist.


 struct Spacing: Hashable { let height: CGFloat } class SpacingView: UIView { lazy var constraint = self.heightAnchor.constraint(equalToConstant: 1) init() { super.init(frame: .zero) self.constraint.isActive = true } } extension Spacing: WidgetHashable { func widgetEqual(_ any: Any) -> Bool { if let spacing = any as? Spacing { return self == spacing } return false } func widgetHash(into hasher: inout Hasher) { self.hash(into: &hasher) } } extension Spacing: WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { let cell: WidgetCell<SpacingView> = collectionView.cellDequeueSafely(indexPath: indexPath) if cell.view == nil { cell.view = SpacingView() } cell.view?.constraint.constant = height return cell } func widgetSize(containerWidth: CGFloat) -> CGSize { return CGSize(width: containerWidth, height: height) } } 

WidgetCell<T> ist nur eine Unterklasse von UICollectionViewCell , die eine UIView akzeptiert und als UIView hinzufügt. cellDequeueSafely(indexPath:) ist eine Funktion, die eine Zelle in einer Sammlung vor der Wiederverwendung registriert, wenn eine Zelle in der Sammlung zuvor nicht registriert wurde. Spacing wird wie zu Beginn der Veröffentlichung beschrieben verwendet.


Nach dem Empfang des Array von Widgets bleibt nur die Bindung an observerWidgets :


 typealias DataSource = RxCollectionViewSectionedAnimatedDataSource<WidgetSection> class Controller: UIViewController { private lazy var dataSource: DataSource = self.makeDataSource() var observerWidgets: (Observable<Widgets>) -> Disposable { return collectionView.rx.items(dataSource: dataSource) } func makeDataSource() -> DataSource { return DataSource(configureCell: { (_, collectionView: UICollectionView, indexPath: IndexPath, widget: Widget) in let cell = widget.widgetCell(collectionView: collectionView, indexPath: indexPath) return cell }) } } 

Ergebnisse


Abschließend möchte ich die eigentliche Arbeit der Sammlung zeigen, die vollständig auf Widgets basiert.


Bild

Wie Sie sehen können, ist die Zerlegung von UICollectionViewCell möglich und kann in geeigneten Situationen das Leben des Entwicklers vereinfachen.


Bemerkungen


Der in der Veröffentlichung angegebene Code ist sehr vereinfacht und sollte nicht kompiliert werden. Ziel war es, den Ansatz zu beschreiben und keine schlüsselfertige Lösung bereitzustellen.


Das WidgetPresentable Protokoll kann um andere Funktionen erweitert werden, die das Layout optimieren, z. B. widgetSizeEstimated(containerWidth:) oder widgetSizePredefined(containerWidth:) , die die geschätzte bzw. feste Größe zurückgeben. Es ist erwähnenswert, dass die Funktion widgetSize(containerWidth:) die Größe des systemLayoutSizeFitting(_:) auch für anspruchsvolle Berechnungen zurückgeben sollte, z. B. für systemLayoutSizeFitting(_:) . Solche Berechnungen können über Dictionary , NSCache usw. zwischengespeichert werden.


Wie Sie wissen, müssen alle von UICollectionView verwendeten UICollectionView in der Sammlung UICollectionView sein. Um jedoch Widgets zwischen verschiedenen Bildschirmen / Sammlungen wiederzuverwenden und nicht alle Zellkennungen / -typen im Voraus zu registrieren, müssen Sie einen Mechanismus erwerben, der eine Zelle unmittelbar vor ihrer ersten Verwendung in jeder Sammlung registriert. In der Publikation wurde hierfür die Funktion cellDequeueSafely(indexPath:) verwendet.


Die Sammlung darf keine Kopf- oder Fußzeilen enthalten. An ihrer Stelle werden Primitive stehen. Das Vorhandensein von Ergänzungsmitteln in der Sammlung bietet im aktuellen Ansatz keine besonderen Boni. Normalerweise werden sie verwendet, wenn das Datenarray genau der Anzahl der Zellen entspricht und Sie zusätzliche Ansichten vor, nach oder zwischen Zellen anzeigen möchten. In unserem Fall können Daten für Hilfsansichten auch zum Widget-Array hinzugefügt und als Grundelemente gezeichnet werden.


Innerhalb derselben Sammlung können Widgets mit denselben Objekten gefunden werden. Zum Beispiel der gleiche Spacing am Anfang und am Ende der Sammlung. Das Vorhandensein solcher nicht eindeutiger Objekte führt dazu, dass die Animation in der Sammlung verschwindet. Um solche Objekte einzigartig zu machen, können Sie spezielle AnyHashable Tags, #file und #line denen das Objekt erstellt wurde usw.

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


All Articles