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:
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:- Adicionar UIView
- Dentro dele, adicione o UIStackView (horizontal)
- Em seguida, adicione UIImageView e UILabel ao UIStackView.
- Para UILabel, defina o valor de Prioridade da resistência à compactação de conteúdo = 1000 para horizontal e vertical.
- 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] } }
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:
- Este protocolo também descreve o método de seleção de células (didSelectItemAt :)
- 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 {
3 elementos em cada linha: class DefaultGriddedContentCollectionViewDelegate: DefaultCollectionViewDelegate { private let itemsPerRow: CGFloat = 3 private let minimumItemSpacing: CGFloat = 8
Combinação de mesa e 3x seguidas. class CustomGriddedContentCollectionViewDelegate: DefaultCollectionViewDelegate { private let itemsPerRow: CGFloat = 3 private let minimumItemSpacing: CGFloat = 8
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.