UICollectionView Around the Head: Changer les vues à la volée

Bonjour, Habr! Je vous présente la traduction de l'article " Tutoriel UICollectionView: Changer de présentation à la volée ".

Dans cet article, nous considérerons l'utilisation de différentes manières d'afficher les éléments, ainsi que leur réutilisation et leur changement dynamique. Ici, nous ne discuterons pas des bases de l'utilisation des collections et de la mise en page automatique.

En conséquence, nous obtenons un exemple:


Lors du développement d'applications mobiles, il y a souvent des situations où la vue tabulaire n'est pas suffisante et où vous devez afficher une liste d'éléments plus intéressants et uniques. De plus, la possibilité de modifier la façon dont les éléments sont affichés peut devenir une «puce» dans votre application.

Toutes les fonctionnalités ci-dessus sont assez simples à implémenter en utilisant UICollectionView et diverses implémentations du protocole UICollectionViewDelegateFlowLayout.

Code de projet complet.

Ce dont nous avons besoin avant tout pour la mise en œuvre:

  • classe FruitsViewController: UICollectionViewController.
  • Modèle de données sur les fruits

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

Cellule avec UIImageView et UILabel pour afficher les fruits


Nous allons créer la cellule dans un fichier séparé avec xib pour réutilisation.

De par leur conception, nous voyons qu'il existe 2 options de cellule possibles - avec le texte ci-dessous et le texte à droite de l'image.



Il peut y avoir des types de cellules complètement différents, auquel cas vous devez créer 2 classes distinctes et utiliser celle souhaitée. Dans notre cas, ce besoin n'existe pas et 1 cellule avec UIStackView suffit.



Étapes pour créer une interface pour une cellule:

  1. Ajouter UIView
  2. À l'intérieur, ajoutez UIStackView (horizontal)
  3. Ensuite, ajoutez UIImageView et UILabel à UIStackView.
  4. Pour UILabel, définissez la valeur de Content Compression Resistance Priority = 1000 pour horizontal et vertical.
  5. Ajoutez 1: 1 pour le rapport d'aspect UIImageView et modifiez la priorité à 750.

Ceci est nécessaire pour un affichage correct en mode horizontal.

Ensuite, nous écrivons la logique d'affichage de notre cellule en mode horizontal et vertical.

Le critère principal pour l'affichage horizontal sera la taille de la cellule elle-même. C'est-à-dire s'il y a assez d'espace - affichez le mode horizontal. Sinon, vertical. Nous supposons qu'il y a suffisamment d'espace lorsque la largeur est 2 fois supérieure à la hauteur, car l'image doit être carrée.

Code de cellule:

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

Passons à la partie principale - au contrôleur et à la logique d'affichage et de commutation des types de cellules.

Pour tous les états d'affichage possibles, créez un style de présentation enum.
Nous ajoutons également un bouton pour basculer entre les états dans la barre de navigation.

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

Tout ce qui concerne la méthode d'affichage des éléments dans une collection est décrit dans le protocole UICollectionViewDelegateFlowLayout. Par conséquent, afin de supprimer toute implémentation du contrôleur et de créer des éléments réutilisables indépendants, nous créerons une implémentation distincte de ce protocole pour chaque type d'affichage.

Cependant, il y a 2 nuances:

  1. Ce protocole décrit également la méthode de sélection des cellules (didSelectItemAt :)
  2. Certaines méthodes et logiques sont les mêmes pour toutes les méthodes de mappage N (dans notre cas, N = 3).

Par conséquent, nous allons créer le protocole CollectionViewSelectableItemDelegate , étendre le protocole UICollectionViewDelegateFlowLayout standard, dans lequel nous définissons la fermeture de la sélection de cellules et, si nécessaire, des propriétés et des méthodes supplémentaires (par exemple, renvoyer le type de cellule si différents types sont utilisés pour les représentations). Cela résoudra le premier problème.

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

Pour résoudre le deuxième problème - avec duplication de la logique, nous allons créer une classe de base avec toute la logique commune:

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

Dans notre cas, la logique générale consiste à appeler une fermeture lors de la sélection d'une cellule, ainsi qu'à changer l'arrière-plan de la cellule lors du passage à l'état en surbrillance .

Ensuite, nous décrivons 3 implémentations des représentations: tabulaire, 3 éléments dans chaque ligne et une combinaison des deux premières méthodes.

Tabulaire :

 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 éléments dans chaque rangée:

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

Combinaison de table et 3x d'affilée.

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

La dernière étape consiste à ajouter les données de vue au contrôleur et à définir le délégué souhaité pour la collection.

Un point important: puisque le délégué de la collection est faible , vous devez avoir un lien fort dans le contrôleur vers l'objet vue.

Dans le contrôleur, créez un dictionnaire de toutes les vues disponibles concernant le type:

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

Et dans la méthode updatePresentationStyle () , ajoutez une modification animée au délégué de collection:

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

C'est tout ce dont nous avons besoin pour que nos éléments se déplacent de manière animée d'une vue à une autre :)


Ainsi, nous pouvons maintenant afficher des éléments sur n'importe quel écran de la manière que nous voulons, basculer dynamiquement entre les affichages et, surtout, le code est indépendant, réutilisable et évolutif.

Code de projet complet.

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


All Articles