UICollectionView dentro de uma UITableViewCell - altura dinâmica?


125

Uma de nossas telas de aplicativos exige que você coloque um UICollectionViewdentro de um UITableViewCell. Isso UICollectionViewterá um número dinâmico de itens, resultando em uma altura que também deve ser calculada dinamicamente. No entanto, estou com problemas para tentar calcular a altura do incorporado UICollectionView.

Nossa abrangência UIViewControllerfoi criada no Storyboards e utiliza o layout automático. Mas, eu não sei como aumentar dinamicamente a altura do com UITableViewCellbase na altura do UICollectionView.

Alguém pode dar algumas dicas ou conselhos sobre como fazer isso?


você pode falar um pouco mais sobre o layout que está tentando realizar? É a altura do UITableViewCelldiferente da sua exibição de tablatura acutal? Você precisa de rolagem vertical em ambos os UICollectionVieweUITableView
apouche

3
A altura do UITableViewCell deve ser dinâmica, com base na altura do UICollectionView que faz parte da célula de exibição de tabela. O layout do meu UICollectionView é de 4 colunas e um número dinâmico de linhas. Portanto, com 100 itens, eu teria 4 colunas e 25 linhas no meu UICollectionView. A altura da célula específica - como indicado pelo heightForRowAtIndexPath do UITableView - precisa ser ajustada com base no tamanho desse UICollectionView. Isso é um pouco mais claro?
Shadowman

@Shadowman, por favor me sugerir uma solução para esta situação
Ravi Ojha

Respostas:


126

A resposta certa é SIM , você pode fazer isso.

Me deparei com esse problema há algumas semanas. Na verdade, é mais fácil do que você imagina. Coloque suas células em NIBs (ou storyboards) e fixe-as para permitir que o layout automático faça todo o trabalho

Dada a seguinte estrutura:

Vista de mesa

TableViewCell

CollectionView

CollectionViewCell

CollectionViewCell

CollectionViewCell

[... número variável de células ou tamanhos diferentes de célula]

A solução é instruir o layout automático a calcular primeiro os tamanhos de collectionViewCell, depois a coleção visualizar contentSize e usá-lo como o tamanho da sua célula. Este é o método UIView que "faz a mágica":

-(void)systemLayoutSizeFittingSize:(CGSize)targetSize 
     withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority 
           verticalFittingPriority:(UILayoutPriority)verticalFittingPriority

Você deve definir aqui o tamanho do TableViewCell, que no seu caso é o contentSize do CollectionView.

CollectionViewCell

No CollectionViewCell, você deve informar a célula para fazer o layout toda vez que alterar o modelo (por exemplo: você define um UILabel com um texto e, em seguida, a célula precisa ser layout novamente).

- (void)bindWithModel:(id)model {
    // Do whatever you may need to bind with your data and 
    // tell the collection view cell's contentView to resize
    [self.contentView setNeedsLayout];
}
// Other stuff here...

TableViewCell

O TableViewCell faz a mágica. Ele possui uma saída para o seu collectionView, habilita o layout automático para as células collectionView usando o estimadoItemSize do UICollectionViewFlowLayout .

Em seguida, o truque é definir o tamanho da célula tableView no método systemLayoutSizeFittingSize .... (NOTA: iOS8 ou posterior)

NOTA: Tentei usar o método de altura da célula delegada do tableView. -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPathMas é tarde demais para o sistema de layout automático calcular o contentSize CollectionView e, às vezes, você pode encontrar células redimensionadas incorretas.

@implementation TableCell

- (void)awakeFromNib {
    [super awakeFromNib];
    UICollectionViewFlowLayout *flow = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;

    // Configure the collectionView
    flow.minimumInteritemSpacing = ...;

    // This enables the magic of auto layout. 
    // Setting estimatedItemSize different to CGSizeZero 
    // on flow Layout enables auto layout for collectionView cells.
    // https://developer.apple.com/videos/play/wwdc2014-226/
    flow.estimatedItemSize = CGSizeMake(1, 1);

    // Disable the scroll on your collection view
    // to avoid running into multiple scroll issues.
    [self.collectionView setScrollEnabled:NO];
}

- (void)bindWithModel:(id)model {
    // Do your stuff here to configure the tableViewCell
    // Tell the cell to redraw its contentView        
    [self.contentView layoutIfNeeded];
}

// THIS IS THE MOST IMPORTANT METHOD
//
// This method tells the auto layout
// You cannot calculate the collectionView content size in any other place, 
// because you run into race condition issues.
// NOTE: Works for iOS 8 or later
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority {

    // With autolayout enabled on collection view's cells we need to force a collection view relayout with the shown size (width)
    self.collectionView.frame = CGRectMake(0, 0, targetSize.width, MAXFLOAT);
    [self.collectionView layoutIfNeeded];

    // If the cell's size has to be exactly the content 
    // Size of the collection View, just return the
    // collectionViewLayout's collectionViewContentSize.

    return [self.collectionView.collectionViewLayout collectionViewContentSize];
}

// Other stuff here...

@end

TableViewController

Lembre-se de ativar o sistema de layout automático para as células tableView no seu TableViewController :

- (void)viewDidLoad {
    [super viewDidLoad];

    // Enable automatic row auto layout calculations        
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    // Set the estimatedRowHeight to a non-0 value to enable auto layout.
    self.tableView.estimatedRowHeight = 10;

}

CRÉDITO: @rbarbera ajudou a resolver isso


4
Sim. Isso funciona. Essa deve ser a resposta aceita.
Csotiriou 7/12

1
Alguém pode me fornecer um código de amostra para ele? i quer mostrar imagens múltiplas em vista recolha vertical, que está dentro da célula tableview ..
Ravi Ojha

4
Finalmente, isso funcionou para mim, mas eu tive que fazer outro truque: cell.bounds = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), 10000);"Simulará" uma célula longa com um collectionView longo, para que o collectionView calcule o tamanho de todas as suas células. Caso contrário, o collectionView calculou apenas os tamanhos visíveis das células e definiu incorretamente a altura do talbeViewCell.
ingaham

20
Ao definir a altura do quadro para o máximo systemLayoutSizeFittingSize, eu encontrei um problema rapidamente ao recarregar a célula, os aplicativos parecem estar presos após o self.collectionView.layoutIfNeeded(). Eu o corrigi definindo a altura do quadro com 1 em vez da altura máxima. Espero que ajude se alguém tropeçar neste problema.
titan

5
Aqui está o que fixa-lo para mim - em systemLayoutSizeFitting, inverter a ordem do que é mostrado na resposta atualmente, de modo que as primeiras collectionView.layoutIfNeeded()corridas, entãocollectionView.frame = CGRect.init(origin: .zero, size: CGSize.init(width: targetSize.width, height: 1))
xaphod

101

Acho que minha solução é muito mais simples do que a proposta por @PabloRomeu.

Etapa 1. Crie uma saída de UICollectionViewpara UITableViewCell subclass, onde UICollectionViewé colocado. Vamos, seu nome serácollectionView

Etapa 2. Adicione o IB para UICollectionViewrestrição de altura e crie saída UITableViewCell subclasstambém. Vamos, o nome será collectionViewHeight.

Etapa 3. Em tableView:cellForRowAtIndexPath:adicionar código:

// deque a cell
cell.frame = tableView.bounds;
[cell layoutIfNeeded];
[cell.collectionView reloadData];
cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height;

1
Você já tentou isso? Não parece possível definir uma restrição de altura na exibição de coleção e fixá-la nas margens da célula da exibição de tabela. Você acaba com poucas restrições (daí um aviso de tamanho zero) ou com restrições conflitantes (uma das quais é ignorada). Por favor, explique como definir as restrições para esta solução.
Dale

2
Eu tenho trabalhado. Eu adiciono restrições para collectionViewtodos os lados UITableViewCelle defino tableView.estimatedRowHeight = 44;etableView.rowHeight = UITableViewAutomaticDimension;
Igor

3
Este funciona como campeão ... simples e eficiente ... obrigado. Se, para alguém que não está funcionando, provavelmente é o motivo, precisamos vincular a parte inferior da visualização da coleção à parte inferior da célula da visualização da tabela. Então ele começará a funcionar. Feliz codificação .. :)
Sebin Roy

2
Isso funcionaria quando a tela é redimensionada, digamos, girando um iPad? Isso poderia alterar potencialmente a altura da exibição de coleção e, portanto, a altura da célula, mas se na tela a célula não for necessariamente recarregada pela exibição de tabela, nunca chame o método cellForRowAtIndexPath: e não lhe dará a chance de altere a constante de restrição. Talvez seja possível forçar um reloadData.
24616 Carl Lindberg

2
Você já tentou diferentes tamanhos de célula collectionView? Esse foi o meu problema. Se você tem diferentes cela de CollectionView tamanhos Não funcionou ... :(
Pablo Romeu

21

As visualizações de tabela e de coleção são UIScrollViewsubclasses e, portanto, não gostam de ser incorporadas a outra visualização de rolagem, enquanto tentam calcular o tamanho do conteúdo, reutilizar células etc.

Eu recomendo que você use apenas uma exibição de coleção para todos os seus objetivos.

Você pode dividi-lo em seções e "tratar" o layout de algumas seções como uma exibição de tabela e outras como uma exibição de coleção. Afinal, não há nada que você não possa alcançar com uma exibição de coleção que você pode com uma exibição de tabela.

Se você tiver um layout de grade básico para as "partes" da exibição da coleção, também poderá usar células de tabela regulares para manipulá-las. Ainda assim, se você não precisar do suporte ao iOS 5, use melhor as visualizações de coleção.


1
Você colocou "tratar" entre aspas, porque NÃO é possível definir layouts diferentes por seção CollectionView, certo? Não encontrei uma opção para definir layouts diferentes para diferentes seções de exibição de coleção. Estou negligenciando algo ou você está falando de responder de maneira diferente a 'layoutAttributesForItemAtIndexPath' para seções diferentes em minha própria subclasse personalizada de UICollectionViewLayout?
Alex da franca

Sim -> Você está falando em responder de maneira diferente a layoutAttributesForItemAtIndexPathdiferentes seções da minha subclasse personalizada de UICollectionViewLayout?
Rivera

2
@ Rivera, sua resposta é mais razoável e parece correta. Eu li essa sugestão em muitos lugares. Você também pode compartilhar algum link para provar ou explicar como realizar isso ou pelo menos alguns indicadores. TIA
thesummersign

3
Sua resposta pressupõe que você está começando do zero e que ainda não possui uma grande tableview complexa na qual precisa se encaixar. A pergunta feita especificamente sobre como colocar uma visualização de coleção em uma tableview. IMO sua resposta não é uma "resposta"
xaphod

2
Esta é uma resposta ruim. Ter visualizações de coleção nas visualizações de tabela é uma coisa tão comum, quase todos os aplicativos fazem isso.
crashoverride777

9

A resposta de Pablo Romeu acima ( https://stackoverflow.com/a/33364092/2704206 ) me ajudou imensamente com meu problema. No entanto, tive que fazer algumas coisas de maneira diferente para que isso funcionasse para o meu problema. Primeiro, eu não precisava ligar com tanta layoutIfNeeded()frequência. Eu só tinha que chamá-lo na collectionViewna systemLayoutSizeFittingfunção.

Em segundo lugar, eu tinha restrições de layout automático em minha exibição de coleção na célula de exibição de tabela para fornecer algum preenchimento. Então eu tive que subtrair as margens inicial e final do targetSize.widthquando definir a collectionView.framelargura do. Eu também tive que adicionar as margens superior e inferior à CGSizealtura do valor de retorno .

Para obter essas constantes de restrição, tive a opção de criar saídas para as restrições, codificar permanentemente suas constantes ou procurá-las por um identificador. Decidi optar pela terceira opção para tornar minha classe de célula de exibição de tabela personalizada facilmente reutilizável. No final, era tudo o que eu precisava para fazer funcionar:

class CollectionTableViewCell: UITableViewCell {

    // MARK: -
    // MARK: Properties
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionViewLayout?.estimatedItemSize = CGSize(width: 1, height: 1)
            selectionStyle = .none
        }
    }

    var collectionViewLayout: UICollectionViewFlowLayout? {
        return collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    }

    // MARK: -
    // MARK: UIView functions
    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()

        let topConstraintConstant = contentView.constraint(byIdentifier: "topAnchor")?.constant ?? 0
        let bottomConstraintConstant = contentView.constraint(byIdentifier: "bottomAnchor")?.constant ?? 0
        let trailingConstraintConstant = contentView.constraint(byIdentifier: "trailingAnchor")?.constant ?? 0
        let leadingConstraintConstant = contentView.constraint(byIdentifier: "leadingAnchor")?.constant ?? 0

        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width - trailingConstraintConstant - leadingConstraintConstant, height: 1)

        let size = collectionView.collectionViewLayout.collectionViewContentSize
        let newSize = CGSize(width: size.width, height: size.height + topConstraintConstant + bottomConstraintConstant)
        return newSize
    }
}

Como função auxiliar para recuperar uma restrição por identificador, adiciono a seguinte extensão:

extension UIView {
    func constraint(byIdentifier identifier: String) -> NSLayoutConstraint? {
        return constraints.first(where: { $0.identifier == identifier })
    }
}

NOTA: Você precisará definir o identificador nessas restrições no seu storyboard ou em qualquer lugar em que elas estejam sendo criadas. A menos que eles tenham uma constante 0, isso não importa. Além disso, como na resposta de Pablo, você precisará usar UICollectionViewFlowLayoutcomo o layout da sua exibição de coleção. Por fim, certifique-se de vincular o collectionViewIBOutlet ao seu storyboard.

Com a célula de exibição de tabela personalizado acima, agora eu posso subclasse-lo em qualquer outra célula exibição de tabela que precisa de uma visão de coleção e tê-lo implementar o UICollectionViewDelegateFlowLayoute UICollectionViewDataSourceprotocolos. Espero que isso seja útil para outra pessoa!


5

Eu li todas as respostas. Isso parece servir a todos os casos.

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()
        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width , height: 1)
        return collectionView.collectionViewLayout.collectionViewContentSize
}

3

Uma alternativa à solução de Pablo Romeu é personalizar o próprio UICollectionView, em vez de fazer o trabalho na célula de exibição de tabela.

O problema subjacente é que, por padrão, uma exibição de coleção não possui tamanho intrínseco e, portanto, não pode informar o layout automático das dimensões a serem usadas. Você pode remediar isso criando uma subclasse personalizada que retorne um tamanho intrínseco útil.

Crie uma subclasse de UICollectionView e substitua os seguintes métodos

override func intrinsicContentSize() -> CGSize {
    self.layoutIfNeeded()

    var size = super.contentSize
    if size.width == 0 || size.height == 0 {
        // return a default size
        size = CGSize(width: 600, height:44)
    }

    return size
 }

override func reloadData() {
    super.reloadData()
    self.layoutIfNeeded()
    self.invalidateIntrinsicContentSize()
}

(Você também deve substituir os métodos relacionados: reloadSections, reloadItemsAtIndexPaths de maneira semelhante a reloadData ())

A chamada layoutIfNeeded força a exibição da coleção a recalcular o tamanho do conteúdo, que pode ser usado como o novo tamanho intrínseco.

Além disso, você precisa lidar explicitamente com as alterações no tamanho da visualização (por exemplo, na rotação do dispositivo) no controlador de visualização de tabela

    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    dispatch_async(dispatch_get_main_queue()) {
        self.tableView.reloadData()
    }
}

bro, por favor, poderia ajudar meu problema stackoverflow.com/questions/44650058/… ?
precisa

3

Eu estava enfrentando o mesmo problema recentemente e quase tentei todas as soluções nas respostas, algumas delas funcionaram e outras não minha principal preocupação com a abordagem @PabloRomeu é que, se você tiver outro conteúdo na célula (além da exibição de coleção) você precisará calcular as alturas e as alturas de suas restrições e retornar o resultado para obter o layout automático e não gosto de calcular as coisas manualmente no meu código. Então, aqui está a solução que funcionou bem para mim sem fazer nenhum cálculo manual no meu código.

na cellForRow:atIndexPathexibição de tabela, faço o seguinte:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    //initialize the the collection view data source with the data
    cell.frame = CGRect.zero
    cell.layoutIfNeeded()
    return cell
}

Acho que o que acontece aqui é forçar a célula tableview a ajustar sua altura após o cálculo da altura da exibição de coleção. (depois de fornecer a data collectionView à fonte de dados)


2

Eu colocaria um método estático na classe de exibição de coleção que retornará um tamanho com base no conteúdo que ela terá. Em seguida, use esse método no heightForRowAtIndexPathpara retornar o tamanho adequado.

Observe também que você pode ter um comportamento estranho ao incorporar esses tipos de viewControllers. Eu fiz isso uma vez e tive alguns problemas estranhos de memória que nunca resolvi.


Para mim, eu estava tentando incorporar um controlador de exibição de tabela dentro de controladores de exibição de coleção. Toda vez que uma célula é reutilizada, ela se lembra de algumas restrições de autolayout que eu apliquei a elas. Foi muito complicado e, finalmente, foi uma péssima ideia. Acho que vou usar apenas uma única exibição de coleção.
Fatuhoku

2

Talvez minha variante seja útil; Estive decidindo esta tarefa nas últimas duas horas. Não pretendo que seja 100% correto ou ideal, mas minha habilidade ainda é muito pequena e gostaria de ouvir comentários de especialistas. Obrigado. Uma observação importante: isso funciona para a tabela estática - é especificada pelo meu trabalho atual. Então, tudo que eu uso é viewWillLayoutSubviews de tableView . E um pouco mais.

private var iconsCellHeight: CGFloat = 500 

func updateTable(table: UITableView, withDuration duration: NSTimeInterval) {
    UIView.animateWithDuration(duration, animations: { () -> Void in
        table.beginUpdates()
        table.endUpdates()
    })
}

override func viewWillLayoutSubviews() {
    if let iconsCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 1)) as? CategoryCardIconsCell {
        let collectionViewContentHeight = iconsCell.iconsCollectionView.contentSize.height
        if collectionViewContentHeight + 17 != iconsCellHeight {
            iconsCellHeight = collectionViewContentHeight + 17
            updateTable(tableView, withDuration: 0.2)
        }
    }
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    switch (indexPath.section, indexPath.row) {
        case ...
        case (1,0):
            return iconsCellHeight
        default:
            return tableView.rowHeight
    }
}
  1. Eu sei que o collectionView está localizado na primeira linha da segunda seção;
  2. Deixe a altura da linha é 17 p. maior que sua altura de conteúdo;
  3. iconsCellHeight é um número aleatório quando o programa é iniciado (eu sei, que na forma retrato ele tem que ser exatamente 392, mas não é importante). Se o conteúdo de collectionView + 17 não for igual a esse número, altere seu valor. Na próxima vez em que esta situação a condição fornecer FALSE;
  4. Afinal atualize o tableView. No meu caso, é a combinação de duas operações (para uma boa atualização de linhas estendidas);
  5. E, claro, no heightForRowAtIndexPath, adicione uma linha ao código.

2

Abordagem mais fácil que eu criei até agora, Créditos para a resposta @igor acima,

Na sua classe tableviewcell, insira este

override func layoutSubviews() {
    self.collectionViewOutlet.constant = self.postPoll.collectionViewLayout.collectionViewContentSize.height
}

e, é claro, altere ooutletviewoutlet com sua tomada na classe da célula


1
A altura da célula não calcular corretamente
Nik Kov

2

Recebo uma ideia do @Igor post e investi meu tempo nisso para o meu projeto com rapidez

Apenas passe isso no seu

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    cell.frame = tableView.bounds
    cell.layoutIfNeeded()
    cell.collectionView.reloadData()
    cell.collectionView.heightAnchor.constraint(equalToConstant: cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
    cell.layoutIfNeeded()
    return cell
}

Adição: se o UICollectionView aparecer instável ao carregar as células.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {       
  //do dequeue stuff
  cell.layer.shouldRasterize = true
  cell.layer.rasterizationScale = UIScreen.main.scale
  return cell
}

0

A solução de Pablo não funcionou muito bem para mim, tive efeitos visuais estranhos (o collectionView não está se ajustando corretamente).

O que funcionou foi ajustar a restrição de altura do collectionView (como um NSLayoutConstraint) para o collectionView contentSize durante layoutSubviews(). Esse é o método chamado quando o autolayout é aplicado à célula.

// Constraint on the collectionView height in the storyboard. Priority set to 999.
@IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!


// Method called by autolayout to layout the subviews (including the collectionView).
// This is triggered with 'layoutIfNeeded()', or by the viewController
// (happens between 'viewWillLayoutSubviews()' and 'viewDidLayoutSubviews()'.
override func layoutSubviews() {

    collectionViewHeightConstraint.constant = collectionView.contentSize.height
    super.layoutSubviews()
}

// Call `layoutIfNeeded()` when you update your UI from the model to trigger 'layoutSubviews()'
private func updateUI() {
    layoutIfNeeded()
}

-3

No seu UITableViewDelegate:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return ceil(itemCount/4.0f)*collectionViewCellHeight;
}

Substitua itemCount e CollectionViewCellHeight pelos valores reais. Se você tiver uma matriz de matrizes, itemCount pode ser:

self.items[indexPath.row].count

Como queiras.


Eu acho que a questão principal é como calcular collectionViewCellHeight antes de renderizar.
Thesummersign

-3

1.Criar célula fictícia.
2. Use o método collectionViewContentSize em UICollectionViewLayout de UICollectionView usando dados atuais.


Deve ser a melhor resposta ... já que não há possibilidade de definir a altura da célula com 'uicollectionview' sem colocar o conteúdo na célula fictícia, obter altura e defini-lo novamente na célula apropriada
Bartłomiej Semańczyk

como eu uso esse método?
Xtrinch

Adicione um exemplo.
Nik Kov

-5

Você pode calcular a altura da coleção com base em suas propriedades, como itemSize, sectionInset, minimumLineSpacing, minimumInteritemSpacing, se o seu collectionViewCelltem a borda de uma regra.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.