UICollectionView alrededor de la cabeza: cambio de vistas sobre la marcha

Hola Habr! Le presento la traducción del artículo " Tutorial UICollectionView: Cambio de presentación sobre la marcha ".

En este artículo, consideraremos el uso de varias formas de mostrar elementos, así como su reutilización y cambio dinámico. Aquí no discutiremos los conceptos básicos del trabajo con colecciones y autolayout.

Como resultado, obtenemos un ejemplo:


Al desarrollar aplicaciones móviles, a menudo hay situaciones en las que la vista de tabla no es suficiente y necesita mostrar una lista de elementos más interesantes y únicos. Además, la capacidad de cambiar la forma en que se muestran los elementos puede convertirse en un "chip" en su aplicación.

Todas las características anteriores son bastante simples de implementar usando UICollectionView y varias implementaciones del protocolo UICollectionViewDelegateFlowLayout.

Código completo del proyecto.

Lo que necesitamos ante todo para la implementación:

  • clase FruitsViewController: UICollectionViewController.
  • Modelo de datos de frutas

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

Celda con UIImageView y UILabel para mostrar frutas


Crearemos la celda en un archivo separado con xib para su reutilización.

Por diseño, vemos que hay 2 opciones de celda posibles: con el texto a continuación y el texto a la derecha de la imagen.



Puede haber tipos completamente diferentes de celdas, en cuyo caso necesita crear 2 clases separadas y usar la deseada. En nuestro caso, no existe tal necesidad y 1 celda con UIStackView es suficiente.



Pasos para crear una interfaz para una celda:

  1. Añadir UIView
  2. En su interior agregue UIStackView (horizontal)
  3. A continuación, agregue UIImageView y UILabel a UIStackView.
  4. Para UILabel, establezca el valor de Prioridad de resistencia de compresión de contenido = 1000 para horizontal y vertical.
  5. Agregue 1: 1 para la relación de aspecto UIImageView y cambie la prioridad a 750.

Esto es necesario para una visualización correcta en modo horizontal.

A continuación, escribimos la lógica para mostrar nuestra celda en modo horizontal y vertical.

El criterio principal para la visualización horizontal será el tamaño de la celda en sí. Es decir si hay suficiente espacio, muestre el modo horizontal. Si no, vertical. Suponemos que hay suficiente espacio cuando el ancho es 2 veces mayor que la altura, ya que la imagen debe ser cuadrada.

Código de celda:

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

Pasemos a la parte principal: al controlador y la lógica para mostrar y cambiar los tipos de celda.

Para todos los estados de visualización posibles, cree una enum PresentationStyle.
También agregamos un botón para cambiar entre estados en la barra de navegación.

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

Todo lo relacionado con el método de visualización de elementos en una colección se describe en el protocolo UICollectionViewDelegateFlowLayout. Por lo tanto, para eliminar cualquier implementación del controlador y crear elementos reutilizables independientes, crearemos una implementación separada de este protocolo para cada tipo de pantalla.

Sin embargo, hay 2 matices:

  1. Este protocolo también describe el método de selección de celda (didSelectItemAt :)
  2. Algunos métodos y lógica son los mismos para todos los métodos de mapeo de N (en nuestro caso, N = 3).

Por lo tanto, crearemos el protocolo CollectionViewSelectableItemDelegate , ampliaremos el protocolo estándar UICollectionViewDelegateFlowLayout , en el que definiremos el cierre de la selección de celdas y, si es necesario, las propiedades y métodos adicionales (por ejemplo, devolver el tipo de celda si se utilizan diferentes tipos para las representaciones). Esto resolverá el primer problema.

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

Para resolver el segundo problema, con la duplicación de la lógica, crearemos una clase base con toda la lógica común:

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

En nuestro caso, la lógica general es llamar a un cierre al seleccionar una celda, así como cambiar el fondo de la celda al cambiar al estado resaltado .

A continuación, describimos 3 implementaciones de las representaciones: tabular, 3 elementos en cada fila y una combinación de los dos primeros métodos.

Tabular :

 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 elementos en cada fila:

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

Combinación de mesa y 3x en una fila.

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

El último paso es agregar los datos de la vista al controlador y establecer el delegado deseado en la colección.

Un punto importante: dado que el delegado de la colección es débil , debe tener un vínculo fuerte en el controlador con el objeto de vista.

En el controlador, cree un diccionario de todas las vistas disponibles con respecto al tipo:

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

Y en el método updatePresentationStyle () , agregue un cambio animado al delegado de la colección:

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

Eso es todo lo que se necesita para que nuestros elementos se muevan animadamente de una vista a otra :)


Por lo tanto, ahora podemos mostrar elementos en cualquier pantalla de la forma que queramos, cambiar dinámicamente entre pantallas y, lo más importante, el código es independiente, reutilizable y escalable.

Código completo del proyecto.

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


All Articles