Parece que o que você está pedindo é uma maneira de usar UICollectionView para produzir um layout como UITableView. Se isso é realmente o que você deseja, a maneira certa de fazer isso é com uma subclasse UICollectionViewLayout personalizada (talvez algo como SBTableLayout ).
Por outro lado, se você está realmente perguntando se há uma maneira limpa de fazer isso com o UICollectionViewFlowLayout padrão, então acredito que não há maneira. Mesmo com as células autodimensionáveis do iOS8, não é simples. O problema fundamental, como você disse, é que a maquinaria do layout de fluxo não fornece nenhuma maneira de fixar uma dimensão e deixar outra responder. (Além disso, mesmo se você pudesse, haveria complexidade adicional em torno da necessidade de duas passagens de layout para dimensionar os rótulos de várias linhas. Isso pode não se encaixar com a forma como as células de autodimensionamento desejam calcular todo o dimensionamento por meio de uma chamada para systemLayoutSizeFittingSize.)
No entanto, se você ainda deseja criar um layout do tipo tableview com um layout de fluxo, com células que determinam seu próprio tamanho e respondem naturalmente à largura da visualização da coleção, é claro que é possível. Ainda existe a forma complicada. Eu fiz isso com uma "célula de dimensionamento", ou seja, um UICollectionViewCell não exibido que o controlador mantém apenas para calcular o tamanho das células.
Esta abordagem tem duas partes. A primeira parte é para o delegado da visualização da coleção calcular o tamanho correto da célula, tomando a largura da visualização da coleção e usando o dimensionamento da célula para calcular a altura da célula.
Em seu UICollectionViewDelegateFlowLayout, você implementa um método como este:
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
// NOTE: here is where we say we want cells to use the width of the collection view
let requiredWidth = collectionView.bounds.size.width
// NOTE: here is where we ask our sizing cell to compute what height it needs
let targetSize = CGSize(width: requiredWidth, height: 0)
/// NOTE: populate the sizing cell's contents so it can compute accurately
self.sizingCell.label.text = items[indexPath.row]
let adequateSize = self.sizingCell.preferredLayoutSizeFittingSize(targetSize)
return adequateSize
}
Isso fará com que a visualização da coleção defina a largura da célula com base na visualização da coleção envolvente, mas, em seguida, peça à célula de dimensionamento para calcular a altura.
A segunda parte é fazer com que a célula de dimensionamento use suas próprias restrições AL para calcular a altura. Isso pode ser mais difícil do que deveria ser, devido à maneira como UILabel de várias linhas exige efetivamente um processo de layout de duas fases. O trabalho é feito no método preferredLayoutSizeFittingSize
, que é assim:
/*
Computes the size the cell will need to be to fit within targetSize.
targetSize should be used to pass in a width.
the returned size will have the same width, and the height which is
calculated by Auto Layout so that the contents of the cell (i.e., text in the label)
can fit within that width.
*/
func preferredLayoutSizeFittingSize(targetSize:CGSize) -> CGSize {
// save original frame and preferredMaxLayoutWidth
let originalFrame = self.frame
let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth
// assert: targetSize.width has the required width of the cell
// step1: set the cell.frame to use that width
var frame = self.frame
frame.size = targetSize
self.frame = frame
// step2: layout the cell
self.setNeedsLayout()
self.layoutIfNeeded()
self.label.preferredMaxLayoutWidth = self.label.bounds.size.width
// assert: the label's bounds and preferredMaxLayoutWidth are set to the width required by the cell's width
// step3: compute how tall the cell needs to be
// this causes the cell to compute the height it needs, which it does by asking the
// label what height it needs to wrap within its current bounds (which we just set).
let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
// assert: computedSize has the needed height for the cell
// Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes."
let newSize = CGSize(width:targetSize.width,height:computedSize.height)
// restore old frame and preferredMaxLayoutWidth
self.frame = originalFrame
self.label.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth
return newSize
}
(Este código foi adaptado do código de amostra da Apple do código de amostra da sessão WWDC2014 em "Visualização de coleção avançada".)
Alguns pontos a serem observados. Ele está usando layoutIfNeeded () para forçar o layout de toda a célula, a fim de calcular e definir a largura do rótulo. Mas isso não é o suficiente. Acredito que você também precise definir preferredMaxLayoutWidth
para que a etiqueta use essa largura com o Layout automático. E só então você pode usar o systemLayoutSizeFittingSize
para fazer com que a célula calcule sua altura levando em consideração o rótulo.
Eu gosto dessa abordagem? Não!! Parece muito complexo e faz o layout duas vezes. Mas, contanto que o desempenho não se torne um problema, prefiro executar o layout duas vezes em tempo de execução do que defini-lo duas vezes no código, o que parece ser a única alternativa.
Minha esperança é que, eventualmente, as células autoadensáveis funcionem de forma diferente e tudo fique muito mais simples.
Projeto de exemplo mostrando isso no trabalho.
Mas por que não usar células autodimensionáveis?
Em teoria, os novos recursos do iOS8 para "células autodimensionáveis" deveriam tornar isso desnecessário. Se você definiu uma célula com Auto Layout (AL), a visualização da coleção deve ser inteligente o suficiente para permitir que ela se dimensione e se disponha corretamente. Na prática, não vi nenhum exemplo que tenha feito isso funcionar com etiquetas de várias linhas. Acho que isso ocorre em parte porque o mecanismo de autodimensionamento das células ainda é problemático.
Mas eu aposto que é principalmente por causa da dificuldade usual do Auto Layout e rótulos, que é que UILabels requerem um processo de layout basicamente de duas etapas. Não está claro para mim como você pode executar as duas etapas com células autodimensionáveis.
E como eu disse, este é realmente um trabalho para um layout diferente. Faz parte da essência do layout de fluxo posicionar coisas que têm um tamanho, em vez de fixar uma largura e permitir que escolham sua altura.
E que tal preferredLayoutAttributesFittingAttributes:?
O preferredLayoutAttributesFittingAttributes:
método é uma pista falsa, eu acho. Isso está lá apenas para ser usado com o novo mecanismo de célula autodimensionável. Portanto, essa não é a resposta, desde que esse mecanismo não seja confiável.
E o que está acontecendo com systemlayoutSizeFittingSize :?
Você está certo, os documentos são confusos.
Os documentos em systemLayoutSizeFittingSize:
e systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:
ambos sugerem que você só deve passar UILayoutFittingCompressedSize
e UILayoutFittingExpandedSize
como o targetSize
. No entanto, a própria assinatura do método, os comentários do cabeçalho e o comportamento das funções indicam que elas estão respondendo ao valor exato do targetSize
parâmetro.
Na verdade, se você definir o UICollectionViewFlowLayoutDelegate.estimatedItemSize
, a fim de ativar o novo mecanismo de célula de autodimensionamento, esse valor parece ser transmitido como targetSize. E UILabel.systemLayoutSizeFittingSize
parece retornar exatamente os mesmos valores que UILabel.sizeThatFits
. Isso é suspeito, visto que o argumento para systemLayoutSizeFittingSize
deve ser um alvo aproximado e o argumento para sizeThatFits:
deve ter um tamanho máximo circunscrito.
Mais recursos
Embora seja triste pensar que tal requisito de rotina deva exigir "recursos de pesquisa", acho que sim. Bons exemplos e discussões são: