UICollectionView ao redor da cabeça: alterando vistas em tempo real

Olá Habr! Apresento a você a tradução do artigo " Tutorial do UICollectionView: alterando a apresentação rapidamente ".

Neste artigo, consideraremos o uso de várias maneiras de exibir elementos, bem como sua reutilização e mudança dinâmica. Aqui não discutiremos os conceitos básicos de trabalho com coleções e layout automático.

Como resultado, obtemos um exemplo:


Ao desenvolver aplicativos móveis, geralmente há situações em que a visualização da tabela não é suficiente e você precisa mostrar uma lista de elementos mais interessantes e exclusivos. Além disso, a capacidade de alterar a maneira como os elementos são exibidos pode se tornar um "chip" em seu aplicativo.

Todos os recursos acima são bastante simples de implementar usando o UICollectionView e várias implementações do protocolo UICollectionViewDelegateFlowLayout.

Código completo do projeto.

O que precisamos antes de tudo para a implementação:

  • classe FruitsViewController: UICollectionViewController.
  • Modelo de dados de frutas

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

Célula com UIImageView e UILabel para exibir frutas


Criaremos a célula em um arquivo separado com o xib para reutilização.

Por design, vemos que existem duas opções de células possíveis - com o texto abaixo e o texto à direita da imagem.



Pode haver tipos completamente diferentes de células; nesse caso, você precisa criar 2 classes separadas e usar a desejada. No nosso caso, não existe essa necessidade e uma célula com o UIStackView é suficiente.



Etapas para criar uma interface para uma célula:

  1. Adicionar UIView
  2. Dentro dele, adicione o UIStackView (horizontal)
  3. Em seguida, adicione UIImageView e UILabel ao UIStackView.
  4. Para UILabel, defina o valor de Prioridade da resistência à compactação de conteúdo = 1000 para horizontal e vertical.
  5. Adicione 1: 1 para o UIImageView Aspect Ratio e altere a prioridade para 750.

Isso é necessário para a exibição correta no modo horizontal.

Em seguida, escrevemos a lógica para exibir nossa célula no modo horizontal e vertical.

O principal critério para exibição horizontal será o tamanho da própria célula. I.e. se houver espaço suficiente - exiba o modo horizontal. Caso contrário, vertical. Assumimos que espaço suficiente é quando a largura é 2 vezes maior que a altura, pois a imagem deve ser quadrada.

Código da célula:

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

Vamos passar para a parte principal - para o controlador e a lógica para exibir e alternar tipos de células.

Para todos os estados de exibição possíveis, crie um enumeração PresentationStyle.
Também adicionamos um botão para alternar entre estados na barra de navegação.

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

Tudo sobre o método de exibição de elementos em uma coleção é descrito no protocolo UICollectionViewDelegateFlowLayout. Portanto, para remover quaisquer implementações do controlador e criar elementos reutilizáveis ​​independentes, criaremos uma implementação separada deste protocolo para cada tipo de exibição.

No entanto, existem 2 nuances:

  1. Este protocolo também descreve o método de seleção de células (didSelectItemAt :)
  2. Alguns métodos e lógica são os mesmos para todos os N métodos de mapeamento (no nosso caso, N = 3).

Portanto, criaremos o protocolo CollectionViewSelectableItemDelegate , estenderemos o protocolo UICollectionViewDelegateFlowLayout padrão, no qual definiremos o fechamento da seleção de células e, se necessário, quaisquer propriedades e métodos adicionais (por exemplo, retornar o tipo de célula se diferentes tipos forem usados ​​para representações). Isso resolverá o primeiro problema.

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

Para resolver o segundo problema - com duplicação de lógica, criaremos uma classe base com toda a lógica comum:

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

No nosso caso, a lógica geral é chamar um fechamento ao selecionar uma célula, além de alterar o plano de fundo da célula ao alternar para o estado destacado .

A seguir, descrevemos 3 implementações das representações: tabular, 3 elementos em cada linha e uma combinação dos dois primeiros 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 em cada linha:

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

Combinação de mesa e 3x seguidas.

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

O último passo é adicionar os dados da visualização ao controlador e definir o delegado desejado para a coleção.

Um ponto importante: como o delegado da coleção é fraco , você deve ter um link forte no controlador para o objeto de exibição.

No controlador, crie um dicionário de todas as visualizações disponíveis em relação ao 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 }() 

E no método updatePresentationStyle () , adicione uma alteração animada ao delegado da coleção:

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

Isso é tudo o que é necessário para que nossos elementos se movam animadamente de uma visualização para outra :)


Assim, agora podemos exibir elementos em qualquer tela da maneira que desejar, alternar dinamicamente entre exibições e, o mais importante, o código é independente, reutilizável e escalável.

Código completo do projeto.

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


All Articles