UICollectionView, células de largura total, permitem autolayout de altura dinâmica?

120

Na vertical UICollectionView,

É possível ter células de largura total , mas permitir que a altura dinâmica seja controlada pelo autolayout ?

Isso me parece talvez a "questão mais importante no iOS sem nenhuma resposta realmente boa."


Importante:

Observe que em 99% dos casos, para obter células de largura total + altura dinâmica de layout automático, basta usar uma visualização de tabela. É tão fácil.


Então, qual é um exemplo de onde você precisa de uma visualização de coleção?

As visualizações de coleção são incrivelmente mais poderosas do que as visualizações de tabela.

Um exemplo simples em que você realmente precisa usar uma visualização de coleção para obter células de largura total + altura dinâmica de layout automático:

Se você animar entre dois layouts em uma visualização de coleção. Por exemplo, entre um layout de 1 e 2 colunas, quando o dispositivo gira.

Esse é um idioma comum e normal no iOS. Infelizmente, isso só pode ser alcançado resolvendo o problema apresentado neste controle de qualidade. : - /

Fattie
fonte
sim é possível com a classe UICollectionViewDelegateFlowLayout
Sunil Prajapati
isso não significaria que você poderia realmente usar um tableView? que funcionalidade adicional seria necessária para você, para complicar isso?
Catalina T.
1
Olá, @CatalinaT. , as visualizações de coleção têm muitos, muitos, muitos recursos adicionais em relação às visualizações de tabela. Um exemplo simples é adicionar física. (Se você não sabe o que quero dizer, é como você faz as listas "saltitantes" no iOS.)
Fattie 01 de
você também pode Aplicar de alguma forma física (como SpringDampinge initialSpringVelocity) na célula da tabela ..... dê uma olhada nesta Lista Bouncy usando tableView ,,,, pastebin.com/pFRQhkrX você pode animar a TableViewCelltabela no willDisplayCell:(UITableViewCell *)cellmétodo delgate @Fattie você pode aceitar a resposta que resolveu seu problema para ajudar outra pessoa que está procurando pelo mesmo problema
Dhiru
ohk, você experimentou a lista Bouncy usando a visualização de lista? @Fattie
Dhiru

Respostas:

124

1. Solução para iOS 13+

Com o Swift 5.1 e iOS 13, você pode usar objetos de Layout Composicional para resolver seu problema.

O código de amostra completo a seguir mostra como exibir multilinhas UILabelem largura total UICollectionViewCell:

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let items = [
        [
            "Lorem ipsum dolor sit amet.",
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        ],
        [
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        ],
        [
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
            "Lorem ipsum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
        ]
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        let size = NSCollectionLayoutSize(
            widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
            heightDimension: NSCollectionLayoutDimension.estimated(44)
        )
        let item = NSCollectionLayoutItem(layoutSize: size)
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: 1)

        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
        section.interGroupSpacing = 10

        let headerFooterSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .absolute(40)
        )
        let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: headerFooterSize,
            elementKind: "SectionHeaderElementKind",
            alignment: .top
        )
        section.boundarySupplementaryItems = [sectionHeader]

        let layout = UICollectionViewCompositionalLayout(section: section)
        collectionView.collectionViewLayout = layout
        collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
        collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView")
    }

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return items.count
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items[section].count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
        cell.label.text = items[indexPath.section][indexPath.row]
        return cell
    }

    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView", for: indexPath) as! HeaderView
        headerView.label.text = "Header"
        return headerView
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: { context in
            self.collectionView.collectionViewLayout.invalidateLayout()
        }, completion: nil)
    }

}

HeaderView.swift

import UIKit

class HeaderView: UICollectionReusableView {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .magenta

        addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

CustomCell.swift

import UIKit

class CustomCell: UICollectionViewCell {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        label.numberOfLines = 0
        backgroundColor = .orange
        contentView.addSubview(label)

        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Exibição esperada:

insira a descrição da imagem aqui


2. Solução para iOS 11+

Com o Swift 5.1 e iOS 11, você pode UICollectionViewFlowLayoutcriar uma subclasse e definir sua estimatedItemSizepropriedade como UICollectionViewFlowLayout.automaticSize(isso informa ao sistema que você deseja lidar com o dimensionamento automático UICollectionViewCell). Você terá que substituir layoutAttributesForElements(in:)e layoutAttributesForItem(at:)para definir a largura das células. Por último, você terá que substituir o preferredLayoutAttributesFitting(_:)método de sua célula e calcular sua altura.

O código completo a seguir mostra como exibir multilinhas UILabelem largura total UIcollectionViewCell(restrito pela UICollectionViewárea segura de e UICollectionViewFlowLayoutinserções de):

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let items = [
        [
            "Lorem ipsum dolor sit amet.",
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        ],
        [
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        ],
        [
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
            "Lorem ipsum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
        ]
    ]
    let customFlowLayout = CustomFlowLayout()

    override func viewDidLoad() {
        super.viewDidLoad()

        customFlowLayout.sectionInsetReference = .fromContentInset // .fromContentInset is default
        customFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        customFlowLayout.minimumInteritemSpacing = 10
        customFlowLayout.minimumLineSpacing = 10
        customFlowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        customFlowLayout.headerReferenceSize = CGSize(width: 0, height: 40)

        collectionView.collectionViewLayout = customFlowLayout
        collectionView.contentInsetAdjustmentBehavior = .always
        collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
        collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView")
    }

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return items.count
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items[section].count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
        cell.label.text = items[indexPath.section][indexPath.row]
        return cell
    }

    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView", for: indexPath) as! HeaderView
        headerView.label.text = "Header"
        return headerView
    }

}

CustomFlowLayout.swift

import UIKit

final class CustomFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributesObjects = super.layoutAttributesForElements(in: rect)?.map{ $0.copy() } as? [UICollectionViewLayoutAttributes]
        layoutAttributesObjects?.forEach({ layoutAttributes in
            if layoutAttributes.representedElementCategory == .cell {
                if let newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame {
                    layoutAttributes.frame = newFrame
                }
            }
        })
        return layoutAttributesObjects
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let collectionView = collectionView else {
            fatalError()
        }
        guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
            return nil
        }

        layoutAttributes.frame.origin.x = sectionInset.left
        layoutAttributes.frame.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right
        return layoutAttributes
    }

}

HeaderView.swift

import UIKit

class HeaderView: UICollectionReusableView {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .magenta

        addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

CustomCell.swift

import UIKit

class CustomCell: UICollectionViewCell {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        label.numberOfLines = 0
        backgroundColor = .orange
        contentView.addSubview(label)

        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
        layoutIfNeeded()
        layoutAttributes.frame.size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
        return layoutAttributes
    }

}

Aqui estão algumas implementações alternativas para preferredLayoutAttributesFitting(_:):

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
    layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
    return layoutAttributes
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    label.preferredMaxLayoutWidth = layoutAttributes.frame.width
    layoutAttributes.frame.size.height = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
    return layoutAttributes
}

Exibição esperada:

insira a descrição da imagem aqui

Imanou Petit
fonte
Isso funcionou bem para mim até adicionar cabeçalhos de seção. Os cabeçalhos de seção estão perdidos e cobrem outras células no iOS 11. Mas funciona bem no iOS 12. Você enfrentou algum problema ao adicionar cabeçalhos de seção?
Nakul,
Muito obrigado. Você salvou minha semana. 🙇‍♂️
Gaetan
1
CollectionView rola para o topo sempre que o tamanho muda
Sajith
1
notícias fantásticas, @ImanouPetit! Eu gostaria que a recompensa fosse maior! Informações fantásticas sobre objetos de layout de composição, obrigado.
Fattie
1
Oi, estou tentando fazer isso. Embora eu esteja definindo o layout no viewdidload como você fez acima, recebo a falha afirmando que o collectionview deve ser inicializado com um parâmetro de layout diferente de nil. Tem algo que estou perdendo? Este é o Xcode 11 para iOS 13.
geistmate
29

Problema

Você está procurando altura automática e também quer ter largura total, não é possível obter os dois usando UICollectionViewFlowLayoutAutomaticSize.

Você deseja fazer usando UICollectionViewisso abaixo é a solução para você.

Solução

Passo I : Calcule a altura esperada da célula

1. Se você tiver apenas UILabel no CollectionViewCellque defina o numberOfLines=0e que calculou a altura esperada de UIlable, passar todos os três o paramters

func heightForLable(text:String, font:UIFont, width:CGFloat) -> CGFloat {
    // pass string, font, LableWidth  
    let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
     label.numberOfLines = 0
     label.lineBreakMode = NSLineBreakMode.byWordWrapping
     label.font = font
     label.text = text
     label.sizeToFit()

     return label.frame.height
}

2. Se você CollectionViewCellcontém apenas UIImageViewe deve ser dinâmico em altura, você precisa obter a altura de UIImage (você UIImageViewdeve ter AspectRatiorestrições)

// this will give you the height of your Image
let heightInPoints = image.size.height
let heightInPixels = heightInPoints * image.scale

3. Se contiver ambos, calcule sua altura e some-os.

ETAPA-II : Retorne o tamanho deCollectionViewCell

1. Adicionar UICollectionViewDelegateFlowLayoutdelegado em seu viewController

2. Implementar o método delegado

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

    // This is just for example, for the scenario Step-I -> 1 
    let yourWidthOfLable=self.view.size.width
    let font = UIFont(name: "Helvetica", size: 20.0)

    var expectedHeight = heightForLable(array[indePath.row], font: font, width:yourWidthOfLable)


    return CGSize(width: view.frame.width, height: expectedHeight)
}

Eu espero que isso te ajude.

Dhiru
fonte
1
É possível ter um UICollectionView de rolagem horizontal com a altura das células aumentando verticalmente com o layout automático?
Eu quero conseguir o mesmo, mas meia largura e altura dinâmica
Mihir Mehta
return CGSize(width: view.frame.width/2, height: expectedHeight) também mantenha o Padding em conta e retorne a largura, caso contrário duas células não caberiam em uma única linha.
Dhiru
@ nr5 Você encontrou a solução? Minha exigência é a mesma que a sua. Obrigado.
Maulik Bhuptani
@MaulikBhuptani Não consegui fazer isso completamente com o Auto Layout. Tive que criar uma classe Layout personalizada e substituir alguns métodos de classe.
nr5 de
18

Existem algumas maneiras de resolver esse problema.

Uma maneira é dar ao layout do fluxo de visualização da coleção um tamanho estimado e calcular o tamanho da célula.

Observação: conforme mencionado nos comentários abaixo, a partir do iOS 10 você não precisa mais fornecer um tamanho estimado para acionar a chamada para uma célula func preferredLayoutAttributesFitting(_ layoutAttributes:). Anteriormente (iOS 9), seria necessário fornecer um tamanho estimado se você quisesse consultar as células prefferedLayoutAttributes.

(supondo que você esteja usando storyboards e a visualização da coleção esteja conectada via IB)

override func viewDidLoad() {
    super.viewDidLoad()
    let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    layout?.estimatedItemSize = CGSize(width: 375, height: 200) // your average cell size
}

Para células simples, isso geralmente será o suficiente. Se o tamanho ainda estiver incorreto, você pode sobrescrever na célula de visualização da coleção func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, o que lhe dará mais controle de granulação fina sobre o tamanho da célula. Observação: você ainda precisará fornecer ao layout do fluxo um tamanho estimado .

Em seguida, substitua func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributespara retornar o tamanho correto.

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
    let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
    let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)
    let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)
    autoLayoutAttributes.frame = autoLayoutFrame
    return autoLayoutAttributes
}

Alternativamente, você pode usar uma célula de dimensionamento para calcular o tamanho da célula no UICollectionViewDelegateFlowLayout.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let width = collectionView.frame.width
    let size = CGSize(width: width, height: 0)
    // assuming your collection view cell is a nib
    // you may also instantiate a instance of our cell from a storyboard
    let sizingCell = UINib(nibName: "yourNibName", bundle: nil).instantiate(withOwner: nil, options: nil).first as! YourCollectionViewCell
    sizingCell.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    sizingCell.frame.size = size
    sizingCell.configure(with: object[indexPath.row]) // what ever method configures your cell
    return sizingCell.contentView.systemLayoutSizeFitting(size, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)
}

Embora esses não sejam exemplos perfeitos de produção, eles devem ajudar você a começar na direção certa. Não posso dizer que essa seja a melhor prática, mas funciona para mim, mesmo com células bastante complexas contendo vários rótulos, que podem ou não quebrar em várias linhas.

Eric Murphey
fonte
@Fattie Não está claro como 'esitmatedItemSize' é irrelevante para uma célula de exibição de coleção de tamanho dinâmico, então, gostaria de ouvir sua opinião. (não é sarcasmo)
Eric Murphey
Além disso, se esta resposta estiver faltando algo específico para ser útil e / ou o que você considera canônico, entre em contato. Não vou discutir a recompensa, gostaria apenas de ser útil para qualquer pessoa que veja esta questão.
Eric Murphey
1
(incluir ou não incluir estimadoItemSize não faz diferença no iOS atual e não ajuda em nada, de qualquer forma, no problema de "largura total + altura dinâmica".) o código no final, os nibs raramente são usados ​​atualmente e tem pouca relevância para uma altura dinâmica (por exemplo, algumas visualizações de texto de altura dinâmica). obrigado
Fattie
Também cheguei a essa solução, mas no meu caso, iOS 11, Xcode 9.2, o conteúdo da célula parecia desconectado da própria célula - não consegui que uma imagem se expandisse para a altura ou largura total da dimensão fixa. Notei ainda que o CV era as restrições de configurações em preferredLayoutAttributesFitting. Em seguida, adicionei uma restrição minha na dita função, vinculando à dimensão fixa de estimadoItemSize, FWIW, veja minha resposta abaixo.
Chris Conover
16

Eu encontrei uma solução muito fácil para esse problema: dentro de meu CollectionViewCell eu tenho um UIView () que é na verdade apenas um plano de fundo. Para obter a largura total, basta definir as seguintes âncoras

bgView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.size.width - 30).isActive = true // 30 is my added up left and right Inset
bgView.topAnchor.constraint(equalTo: topAnchor).isActive = true
bgView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
bgView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
bgView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true

A "mágica" acontece na primeira linha. Eu defino o widthAnchor dinamicamente para a largura da tela. Também é importante subtrair as inserções de seu CollectionView. Caso contrário, o celular não aparecerá. Se você não quiser ter essa visão de plano de fundo, basta torná-la invisível.

O FlowLayout usa as seguintes configurações

layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

O resultado é uma célula de largura total com altura dinâmica.

insira a descrição da imagem aqui

inf1783
fonte
1
vaca sagrada - isso é incrível! tão óbvio ! brilhante!
Fattie
1
apenas TBC @ inf1783, você está fazendo isso no UICollectionView? (não é visualização de tabela) - correto?
Fattie de
1
@Fattie Sim, é um UICollectionView;)
inf1783
fantástico @ inf1783, mal posso esperar para experimentar. A propósito, outra boa maneira de fazer isso é criar uma subclasse de UIView e simplesmente adicionar uma largura intrínseca (estou supondo que teria o mesmo efeito, e provavelmente também funcionaria na hora do storyboard)
Fattie
1
Sempre que tento fazer isso acabo em um loop infinito com o erro O comportamento do UICollectionViewFlowLayout não está definido: /
Skodik.o
7

TRABALHANDO!!! Testado em IOS: 12.1 Swift 4.1

Eu tenho uma solução muito simples que funciona sem quebrar as restrições.

insira a descrição da imagem aqui

Minha ViewControllerClass

class ViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!

    let cellId = "CustomCell"

    var source = ["nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,","nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,","nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,"]

    override func viewDidLoad() {
        super.viewDidLoad()

        self.collectionView.delegate = self
        self.collectionView.dataSource = self
        self.collectionView.register(UINib.init(nibName: cellId, bundle: nil), forCellWithReuseIdentifier: cellId)

        if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        }

    }

}


extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.source.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as? CustomCell else { return UICollectionViewCell() }
        cell.setData(data: source[indexPath.item])
        return cell
    }


}

Classe CustomCell:

class CustomCell: UICollectionViewCell {

    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var widthConstraint: NSLayoutConstraint!

    override func awakeFromNib() {
        super.awakeFromNib()
        self.widthConstraint.constant = UIScreen.main.bounds.width
    }

    func setData(data: String) {
        self.label.text = data
    }

    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        return contentView.systemLayoutSizeFitting(CGSize(width: self.bounds.size.width, height: 1))
    }

}

Ingrediente principal é o systemLayoutSizeFitting função em Customcell. E também temos que definir a largura da visualização dentro da célula com restrições.

Abhishek Biswas
fonte
6

Você deve adicionar uma restrição de largura a CollectionViewCell

class SelfSizingCell: UICollectionViewCell {

  override func awakeFromNib() {
      super.awakeFromNib()
      contentView.translatesAutoresizingMaskIntoConstraints = false
      contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
  }
}
Mecídeo
fonte
finalmente alguém o tornou perfeito em duas linhas
Omar N Shamali
Isso limitará a célula a uma largura fixa, dificilmente autodimensionada. Se você girar o dispositivo ou ajustar o tamanho da tela em um iPad, ele permanecerá fixo.
Thomas Verbeek
4
  1. Conjunto estimatedItemSizedo seu layout de fluxo:

    collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
  2. Defina uma restrição de largura na célula e defina-a para ser igual à largura da visualização:

    class CollectionViewCell: UICollectionViewCell {
        private var widthConstraint: NSLayoutConstraint?
    
        ...
    
        override init(frame: CGRect) {
            ...
            // Create width constraint to set it later.
            widthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0)
        }
    
        override func updateConstraints() {
            // Set width constraint to superview's width.
            widthConstraint?.constant = superview?.bounds.width ?? 0
            widthConstraint?.isActive = true
            super.updateConstraints()
        }
    
        ...
    }

Exemplo completo

Testado em iOS 11.

romano
fonte
3

Pessoalmente, descobri que as melhores maneiras de ter um UICollectionView onde o AutoLayout determina o tamanho enquanto cada Cell pode ter um tamanho diferente é implementar a função UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath enquanto usa uma Cell real para medir o tamanho.

Falei sobre isso em um de meus posts

Esperançosamente, este o ajudará a alcançar o que deseja. Não tenho 100% de certeza, mas acredito que ao contrário de UITableView, onde você pode realmente ter uma altura totalmente automática de células usando AutoLayout em conjunto com

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44

UICollectionView não permite que o AutoLayout determine o tamanho, porque UICollectionViewCell não preenche necessariamente toda a largura da tela.

Mas aqui vai uma pergunta para você : se você precisa de células com largura de tela inteira, por que você se incomoda em usar o UICollectionView em vez de um bom e velho UITableView que vem com células de dimensionamento automático?

xxtesaxx
fonte
espere, Aprit abaixo está dizendo que UICollectionViewFlowLayoutAutomaticSize agora está disponível em layout de fluxo, no iOS10 ...
Fattie
Eu também acabei de ver isso. Aparentemente, isso é uma coisa agora no iOS 10. Acabei de assistir a palestra WWDC correspondente sobre isso: developer.apple.com/videos/play/wwdc2016/219 No entanto, isso irá dimensionar automaticamente a célula para caber em seu conteúdo. Não tenho certeza de como você poderia dizer à célula (com AutoLayout) para preencher a largura da tela, uma vez que você não pode configurar uma restrição entre o UICollectionViewCell e seu pai (pelo menos não no
StoryBoards
certo. parecemos não estar mais perto de uma solução real para esse problema incrivelmente óbvio. : /
Fattie
De acordo com a conversa, você ainda pode sobrescrever sizeThatFits () ou preferredLayoutAttributesFitting () e calcular o tamanho por si mesmo, mas acho que não foi isso que o OP pediu. De qualquer forma, eu ainda estaria interessado no caso de uso para quando ele / ela precisa de um UICollectionViewCell de largura total e por que seria tão importante não usar um UITableView neste caso específico (claro que pode haver certos casos, mas então eu acho você apenas tem que fazer alguns cálculos sozinho)
xxtesaxx
pensando nisso, é incrível que você não possa simplesmente definir "colunas == 1" (e talvez colunas == 2 quando o dispositivo está de lado), com visualizações de coleção.
Fattie
3

Por meu comentário sobre a resposta de Eric, minha solução é muito semelhante à dele, mas tive que adicionar uma restrição em preferredSizeFor ... para restringir à dimensão fixa.

    override func systemLayoutSizeFitting(
        _ targetSize: CGSize, withHorizontalFittingPriority
        horizontalFittingPriority: UILayoutPriority,
        verticalFittingPriority: UILayoutPriority) -> CGSize {

        width.constant = targetSize.width

        let size = contentView.systemLayoutSizeFitting(
            CGSize(width: targetSize.width, height: 1),
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: verticalFittingPriority)

        print("\(#function) \(#line) \(targetSize) -> \(size)")
        return size
    }

Esta pergunta tem várias duplicatas, respondi em detalhes aqui e forneci um aplicativo de amostra funcional aqui.

Chris Conover
fonte
2

Não tenho certeza se isso se qualifica como uma "resposta realmente boa", mas é o que estou usando para fazer isso. Meu layout de fluxo é horizontal e estou tentando fazer o ajuste de largura com autolayout, então é semelhante à sua situação.

extension PhotoAlbumVC: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    // My height is static, but it could use the screen size if you wanted
    return CGSize(width: collectionView.frame.width - sectionInsets.left - sectionInsets.right, height: 60) 
  }
}

Em seguida, no controlador de visualização em que a restrição autolayout é modificada, eu disparo um NSNotification.

NotificationCenter.default.post(name: NSNotification.Name("constraintMoved"), object: self, userInfo: nil)

Em minha subclasse UICollectionView, eu escuto essa notificação:

// viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(handleConstraintNotification(notification:)), name: NSNotification.Name("constraintMoved"), object: nil)

e invalidar o layout:

func handleConstraintNotification(notification: Notification) {
    self.collectionView?.collectionViewLayout.invalidateLayout()
}

Isso faz sizeForItemAtcom que seja chamado novamente usando o novo tamanho da visualização da coleção. No seu caso, ele deve ser capaz de atualizar dadas as novas restrições disponíveis no layout.

Mark Suman
fonte
apenas TBC Mark, você quer dizer que está realmente mudando o tamanho de uma célula? (ou seja, a célula aparece e tem tamanho X, então algo acontece em seu aplicativo e torna-se tamanho Y ..?) thx
Fattie
Sim. Quando o usuário arrasta um elemento na tela, é disparado um evento que atualiza o tamanho da célula.
Mark Suman
1

Em seu viewDidLayoutSubviews, defina a estimatedItemSizelargura total (layout refere-se ao objeto UICollectionViewFlowLayout):

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
    return CGSize(width: collectionView.bounds.size.width, height: 120)
}

Em sua célula, certifique-se de que suas restrições tocam as partes superior e inferior da célula (o código a seguir usa cartografia para simplificar a configuração das restrições, mas você pode fazer isso com NSLayoutConstraint ou IB se desejar):

constrain(self, nameLabel, valueLabel) { view, name, value in
        name.top == view.top + 10
        name.left == view.left
        name.bottom == view.bottom - 10
        value.right == view.right
        value.centerY == view.centerY
    }

Voila, suas células irão crescer automaticamente em altura!

Rafael
fonte
0

Nenhuma das soluções funcionou para mim, pois preciso da largura dinâmica para me adaptar à largura dos iPhones.

    class CustomLayoutFlow: UICollectionViewFlowLayout {
        override init() {
            super.init()
            minimumInteritemSpacing = 1 ; minimumLineSpacing = 1 ; scrollDirection = .horizontal
        }

        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            minimumInteritemSpacing = 1 ; minimumLineSpacing = 1 ; scrollDirection = .horizontal
        }

        override var itemSize: CGSize {
            set { }
            get {
                let width = (self.collectionView?.frame.width)!
                let height = (self.collectionView?.frame.height)!
                return CGSize(width: width, height: height)
            }
        }
    }

    class TextCollectionViewCell: UICollectionViewCell {
        @IBOutlet weak var textView: UITextView!

        override func prepareForReuse() {
            super.prepareForReuse()
        }
    }




    class IntroViewController: UIViewController, UITextViewDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UINavigationControllerDelegate {
        @IBOutlet weak var collectionViewTopDistanceConstraint: NSLayoutConstraint!
        @IBOutlet weak var collectionViewTopDistanceConstraint: NSLayoutConstraint!
        @IBOutlet weak var collectionView: UICollectionView!
        var collectionViewLayout: CustomLayoutFlow!

        override func viewDidLoad() {
            super.viewDidLoad()

            self.collectionViewLayout = CustomLayoutFlow()
            self.collectionView.collectionViewLayout = self.collectionViewLayout
        }

        override func viewWillLayoutSubviews() {
            self.collectionViewTopDistanceConstraint.constant = UIScreen.main.bounds.height > 736 ? 94 : 70

            self.view.layoutIfNeeded()
        }
    }
Fluxo de iOS
fonte
0

Do iOS 10, temos uma nova API no layout de fluxo para fazer isso.

Tudo o que você precisa fazer é definir o seu flowLayout.estimatedItemSizepara uma nova constante UICollectionViewFlowLayoutAutomaticSize,.

Fonte

Arpit Dongre
fonte
Hmm, você tem certeza de que é * largura total ? Então, essencialmente uma coluna?
Fattie
Poderia se você definir a largura de UICollectionViewCell para largura total explicitamente.
Arpit Dongre
Não, ele não fixa a largura para a altura total e ignora o valor do tamanho estimado, a menos que você siga a resposta de Eric acima ou a minha abaixo / mais tarde.
Chris Conover
OK, infelizmente isso não tem absolutamente nenhuma conexão com o problema! heh! : O
Fattie
0

AutoLayout pode ser usado para dimensionamento automático de células em CollectionView em 2 etapas fáceis:

  1. Habilitando o dimensionamento dinâmico de células

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

  1. Tenha uma visualização de contêiner e defina containerView.widthAnchor.constraint de collectionView(:cellForItemAt:)para limitar a largura de contentView à largura de collectionView.
class ViewController: UIViewController, UICollectionViewDataSource {
    ...

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! MultiLineCell
        cell.textView.text = dummyTextMessages[indexPath.row]
        cell.maxWidth = collectionView.frame.width
        return cell
    }

    ...
}
class MultiLineCell: UICollectionViewCell{
    ....

    var maxWidth: CGFloat? {
        didSet {
            guard let maxWidth = maxWidth else {
                return
            }
            containerViewWidthAnchor.constant = maxWidth
            containerViewWidthAnchor.isActive = true
        }
    }

    ....
}

É isso que você obterá o resultado desejado. Consulte a essência a seguir para obter o código completo:

Referência / Créditos:

Captura de tela: insira a descrição da imagem aqui

JolasJoe
fonte
-6

Você deve herdar a classe UICollectionViewDelegateFlowLayout em seu collectionViewController. Em seguida, adicione a função:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: view.frame.width, height: 100)
}

Usando isso, você tem o tamanho da largura da largura da tela.

E agora você tem um collectionViewController com linhas como um tableViewController.

Se você quiser que o tamanho da altura de cada célula seja dinamicamente, talvez você deva criar células personalizadas para cada célula de que precisa.

Edu
fonte
7
Esta não é exatamente a resposta para a pergunta :) Isso daria uma altura fixa de 100, ou seja, é como um programado antes mesmo de o autolayout existir.
Fattie