UICollectionView um den Kopf: Wechselnde Ansichten im laufenden Betrieb

Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Artikels " UICollectionView Tutorial: Ändern der Präsentation im laufenden Betrieb ".

In diesem Artikel werden wir die Verwendung verschiedener Arten der Anzeige von Elementen sowie deren Wiederverwendung und dynamische Änderung betrachten. Hier werden wir nicht die Grundlagen der Arbeit mit Sammlungen und Autolayout diskutieren.

Als Ergebnis erhalten wir ein Beispiel:


Bei der Entwicklung mobiler Anwendungen kommt es häufig vor, dass die Tabellenansicht nicht ausreicht und Sie eine Liste mit interessanteren und einzigartigeren Elementen anzeigen müssen. Darüber hinaus kann die Möglichkeit, die Darstellung von Elementen zu ändern, zu einem „Chip“ in Ihrer Anwendung werden.

Alle oben genannten Funktionen sind mit UICollectionView und verschiedenen Implementierungen des UICollectionViewDelegateFlowLayout-Protokolls recht einfach zu implementieren.

Vollständiger Projektcode.

Was wir vor allem für die Umsetzung brauchen:

  • Klasse FruitsViewController: UICollectionViewController.
  • Fruchtdatenmodell

    struct Fruit { let name: String let icon: UIImage } 
  • Klasse FruitCollectionViewCell: UICollectionViewCell

Zelle mit UIImageView und UILabel zur Anzeige von Früchten


Wir werden die Zelle in einer separaten Datei mit xib zur Wiederverwendung erstellen.

Wir sehen, dass es zwei mögliche Zellenoptionen gibt - mit Text unten und Text rechts neben dem Bild.



Es kann völlig unterschiedliche Zelltypen geben. In diesem Fall müssen Sie zwei separate Klassen erstellen und die gewünschte verwenden. In unserem Fall besteht keine solche Notwendigkeit und 1 Zelle mit UIStackView ist ausreichend.



Schritte zum Erstellen einer Schnittstelle für eine Zelle:

  1. UIView hinzufügen
  2. Fügen Sie darin UIStackView hinzu (horizontal)
  3. Fügen Sie als Nächstes UIImageView und UILabel zu UIStackView hinzu.
  4. Setzen Sie für UILabel den Wert für Content Compression Resistance Priority = 1000 für horizontal und vertikal.
  5. Fügen Sie 1: 1 für das UIImageView-Seitenverhältnis hinzu und ändern Sie die Priorität auf 750.

Dies ist für eine korrekte Anzeige im horizontalen Modus erforderlich.

Als nächstes schreiben wir die Logik für die Anzeige unserer Zelle sowohl im horizontalen als auch im vertikalen Modus.

Das Hauptkriterium für die horizontale Anzeige ist die Größe der Zelle selbst. Das heißt, Wenn genügend Platz vorhanden ist, zeigen Sie den horizontalen Modus an. Wenn nicht, vertikal. Wir gehen davon aus, dass genügend Platz vorhanden ist, wenn die Breite zweimal größer als die Höhe ist, da das Bild quadratisch sein sollte.

Zellcode:

 class FruitCollectionViewCell: UICollectionViewCell { static let reuseID = String(describing: FruitCollectionViewCell.self) static let nib = UINib(nibName: String(describing: FruitCollectionViewCell.self), bundle: nil) @IBOutlet private weak var stackView: UIStackView! @IBOutlet private weak var ibImageView: UIImageView! @IBOutlet private weak var ibLabel: UILabel! override func awakeFromNib() { super.awakeFromNib() backgroundColor = .white clipsToBounds = true layer.cornerRadius = 4 ibLabel.font = UIFont.systemFont(ofSize: 18) } override func layoutSubviews() { super.layoutSubviews() updateContentStyle() } func update(title: String, image: UIImage) { ibImageView.image = image ibLabel.text = title } private func updateContentStyle() { let isHorizontalStyle = bounds.width > 2 * bounds.height let oldAxis = stackView.axis let newAxis: NSLayoutConstraint.Axis = isHorizontalStyle ? .horizontal : .vertical guard oldAxis != newAxis else { return } stackView.axis = newAxis stackView.spacing = isHorizontalStyle ? 16 : 4 ibLabel.textAlignment = isHorizontalStyle ? .left : .center let fontTransform: CGAffineTransform = isHorizontalStyle ? .identity : CGAffineTransform(scaleX: 0.8, y: 0.8) UIView.animate(withDuration: 0.3) { self.ibLabel.transform = fontTransform self.layoutIfNeeded() } } } 

Kommen wir zum Hauptteil - zur Steuerung und zur Logik zum Anzeigen und Umschalten von Zelltypen.

Erstellen Sie für alle möglichen Anzeigezustände einen enum PresentationStyle.
Wir fügen auch eine Schaltfläche hinzu, um zwischen den Zuständen in der Navigationsleiste zu wechseln.

 class FruitsViewController: UICollectionViewController { private enum PresentationStyle: String, CaseIterable { case table case defaultGrid case customGrid var buttonImage: UIImage { switch self { case .table: return imageLiteral(resourceName: "table") case .defaultGrid: return imageLiteral(resourceName: "default_grid") case .customGrid: return imageLiteral(resourceName: "custom_grid") } } } private var selectedStyle: PresentationStyle = .table { didSet { updatePresentationStyle() } } private var datasource: [Fruit] = FruitsProvider.get() override func viewDidLoad() { super.viewDidLoad() self.collectionView.register(FruitCollectionViewCell.nib, forCellWithReuseIdentifier: FruitCollectionViewCell.reuseID) collectionView.contentInset = .zero updatePresentationStyle() navigationItem.rightBarButtonItem = UIBarButtonItem(image: selectedStyle.buttonImage, style: .plain, target: self, action: #selector(changeContentLayout)) } private func updatePresentationStyle() { navigationItem.rightBarButtonItem?.image = selectedStyle.buttonImage } @objc private func changeContentLayout() { let allCases = PresentationStyle.allCases guard let index = allCases.firstIndex(of: selectedStyle) else { return } let nextIndex = (index + 1) % allCases.count selectedStyle = allCases[nextIndex] } } // MARK: UICollectionViewDataSource & UICollectionViewDelegate extension FruitsViewController { override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return datasource.count } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FruitCollectionViewCell.reuseID, for: indexPath) as? FruitCollectionViewCell else { fatalError("Wrong cell") } let fruit = datasource[indexPath.item] cell.update(title: fruit.name, image: fruit.icon) return cell } } 

Alles, was die Methode zum Anzeigen von Elementen in einer Sammlung betrifft, ist im UICollectionViewDelegateFlowLayout-Protokoll beschrieben. Um Implementierungen von der Steuerung zu entfernen und unabhängige wiederverwendbare Elemente zu erstellen, erstellen wir daher für jeden Anzeigetyp eine separate Implementierung dieses Protokolls.

Es gibt jedoch 2 Nuancen:

  1. Dieses Protokoll beschreibt auch die Zellauswahlmethode (didSelectItemAt :)
  2. Einige Methoden und Logik sind für alle N Mapping-Methoden gleich (in unserem Fall N = 3).

Daher erstellen wir das CollectionViewSelectableItemDelegate- Protokoll, erweitern das Standardprotokoll UICollectionViewDelegateFlowLayout , in dem wir den Abschluss der Zellenauswahl und gegebenenfalls zusätzliche Eigenschaften und Methoden definieren (z. B. Rückgabe des Zelltyps, wenn unterschiedliche Typen für Darstellungen verwendet werden). Dies wird das erste Problem lösen.

 protocol CollectionViewSelectableItemDelegate: class, UICollectionViewDelegateFlowLayout { var didSelectItem: ((_ indexPath: IndexPath) -> Void)? { get set } } 

Um das zweite Problem zu lösen - mit der Verdoppelung der Logik erstellen wir eine Basisklasse mit der gesamten gemeinsamen Logik:

 class DefaultCollectionViewDelegate: NSObject, CollectionViewSelectableItemDelegate { var didSelectItem: ((_ indexPath: IndexPath) -> Void)? let sectionInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 20.0, right: 16.0) func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { didSelectItem?(indexPath) } func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) { let cell = collectionView.cellForItem(at: indexPath) cell?.backgroundColor = UIColor.clear } func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) { let cell = collectionView.cellForItem(at: indexPath) cell?.backgroundColor = UIColor.white } } 

In unserem Fall besteht die allgemeine Logik darin, beim Auswählen einer Zelle einen Abschluss aufzurufen und den Hintergrund der Zelle zu ändern, wenn in den hervorgehobenen Zustand gewechselt wird.

Als nächstes beschreiben wir 3 Implementierungen der Darstellungen: tabellarisch, 3 Elemente in jeder Zeile und eine Kombination der ersten beiden Methoden.

Tabellarisch :

 class TabledContentCollectionViewDelegate: DefaultCollectionViewDelegate { // MARK: - UICollectionViewDelegateFlowLayout func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let paddingSpace = sectionInsets.left + sectionInsets.right let widthPerItem = collectionView.bounds.width - paddingSpace return CGSize(width: widthPerItem, height: 112) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { return sectionInsets } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return 10 } } 

3 Elemente in jeder Zeile:

 class DefaultGriddedContentCollectionViewDelegate: DefaultCollectionViewDelegate { private let itemsPerRow: CGFloat = 3 private let minimumItemSpacing: CGFloat = 8 // MARK: - UICollectionViewDelegateFlowLayout func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let paddingSpace = sectionInsets.left + sectionInsets.right + minimumItemSpacing * (itemsPerRow - 1) let availableWidth = collectionView.bounds.width - paddingSpace let widthPerItem = availableWidth / itemsPerRow return CGSize(width: widthPerItem, height: widthPerItem) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { return sectionInsets } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return 20 } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return minimumItemSpacing } } 

Kombination von Tisch und 3x hintereinander.

 class CustomGriddedContentCollectionViewDelegate: DefaultCollectionViewDelegate { private let itemsPerRow: CGFloat = 3 private let minimumItemSpacing: CGFloat = 8 // MARK: - UICollectionViewDelegateFlowLayout func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let itemSize: CGSize if indexPath.item % 4 == 0 { let itemWidth = collectionView.bounds.width - (sectionInsets.left + sectionInsets.right) itemSize = CGSize(width: itemWidth, height: 112) } else { let paddingSpace = sectionInsets.left + sectionInsets.right + minimumItemSpacing * (itemsPerRow - 1) let availableWidth = collectionView.bounds.width - paddingSpace let widthPerItem = availableWidth / itemsPerRow itemSize = CGSize(width: widthPerItem, height: widthPerItem) } return itemSize } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { return sectionInsets } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return 20 } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return minimumItemSpacing } } 

Der letzte Schritt besteht darin, die Ansichtsdaten zum Controller hinzuzufügen und den gewünschten Delegaten für die Sammlung festzulegen.

Ein wichtiger Punkt: Da der Delegat der Sammlung schwach ist , müssen Sie im Controller eine starke Verknüpfung zum Ansichtsobjekt haben.

Erstellen Sie im Controller ein Wörterbuch aller verfügbaren Ansichten zum Typ:

 private var styleDelegates: [PresentationStyle: CollectionViewSelectableItemDelegate] = { let result: [PresentationStyle: CollectionViewSelectableItemDelegate] = [ .table: TabledContentCollectionViewDelegate(), .defaultGrid: DefaultGriddedContentCollectionViewDelegate(), .customGrid: CustomGriddedContentCollectionViewDelegate(), ] result.values.forEach { $0.didSelectItem = { _ in print("Item selected") } } return result }() 

Fügen Sie in der updatePresentationStyle () -Methode dem Auflistungsdelegierten eine animierte Änderung hinzu:

  collectionView.delegate = styleDelegates[selectedStyle] collectionView.performBatchUpdates({ collectionView.reloadData() }, completion: nil) 

Das ist alles, was unsere Elemente benötigen, um sich animiert von einer Ansicht zur anderen zu bewegen :)


So können wir jetzt Elemente auf beliebige Weise auf jedem Bildschirm anzeigen, dynamisch zwischen Anzeigen wechseln und vor allem ist der Code unabhängig, wiederverwendbar und skalierbar.

Vollständiger Projektcode.

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


All Articles