UICollectionView, celdas de ancho completo, ¿permiten altura dinámica de diseño automático?

120

En una vertical UICollectionView,

¿Es posible tener celdas de ancho completo , pero permitir que la altura dinámica sea ​​controlada por el diseño automático ?

Esto me parece quizás la "pregunta más importante en iOS sin una respuesta realmente buena".


Importante:

Tenga en cuenta que en el 99% de los casos, para lograr celdas de ancho completo + altura dinámica de diseño automático, simplemente use una vista de tabla. Es fácil.


Entonces, ¿cuál es un ejemplo de dónde necesita una vista de colección?

Las vistas de colección son increíblemente más poderosas que las vistas de tabla.

Un ejemplo sencillo en el que realmente tiene que usar una vista de colección para lograr celdas de ancho completo + altura dinámica de diseño automático:

Si anima entre dos diseños en una vista de colección. Por ejemplo, entre un diseño de 1 y 2 columnas, cuando el dispositivo gira.

Ese es un modismo común y normal en iOS. Desafortunadamente, solo se puede lograr resolviendo el problema planteado en este QA. : - /

Fattie
fuente
sí es posible con la clase UICollectionViewDelegateFlowLayout
Sunil Prajapati
¿No significaría esto que realmente podría usar un tableView? ¿Qué funcionalidad adicional sería necesaria para usted para complicar demasiado esto?
Catalina T.
1
Hola @CatalinaT. , las vistas de colección tienen muchas, muchas, muchas características adicionales sobre las vistas de tabla. Un ejemplo sencillo es la adición de física. (Si no sabes a qué me refiero, así es como haces las listas "hinchables" en iOS.)
Fattie
también puede Aplicar de alguna manera la física (como SpringDampingy initialSpringVelocity) en la celda de la tabla ... eche un vistazo a esta lista hinchable usando tableView ,,,, pastebin.com/pFRQhkrX puede animar la TableViewCelltabla en el willDisplayCell:(UITableViewCell *)cellmétodo delgate @Fattie puede aceptar la respuesta que resolvió su problema para ayudar a otros que buscan el mismo problema
Dhiru
ohk, ¿probaste la lista Bouncy usando la vista de lista? @Fattie
Dhiru

Respuestas:

123

1. Solución para iOS 13+

Con Swift 5.1 y iOS 13, puede usar objetos de diseño de composición para resolver su problema.

El siguiente código de muestra completo muestra cómo mostrar varias líneas UILabeldentro de ancho completo 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")
    }

}

Pantalla esperada:

ingrese la descripción de la imagen aquí


2. Solución para iOS 11+

Con Swift 5.1 y iOS 11, puede UICollectionViewFlowLayoutcrear una subclase y establecer su estimatedItemSizepropiedad en UICollectionViewFlowLayout.automaticSize(esto le dice al sistema que desea lidiar con el tamaño automático UICollectionViewCell). Luego tendrá que anular layoutAttributesForElements(in:)y layoutAttributesForItem(at:)para establecer el ancho de las celdas. Por último, tendrá que anular el preferredLayoutAttributesFitting(_:)método de su celda y calcular su altura.

El siguiente código completo muestra cómo mostrar varias líneas UILabeldentro de ancho completo UIcollectionViewCell(restringido por UICollectionViewel área segura y UICollectionViewFlowLayoutlos insertos):

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
    }

}

Aquí hay algunas implementaciones 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
}

Pantalla esperada:

ingrese la descripción de la imagen aquí

Imanou Petit
fuente
Esto funcionó bien para mí hasta que agregué encabezados de sección. Los encabezados de sección están fuera de lugar y cubren otras celdas en iOS 11. Pero funciona bien en iOS 12. ¿Tuviste algún problema al agregar encabezados de sección?
Nakul
Muchas gracias. Salvaste mi semana. 🙇‍♂️
Gaetan
1
CollectionView se desplaza hacia arriba cada vez que cambia el tamaño
Sajith
1
fantásticas noticias, @ImanouPetit! ¡Ojalá la recompensa fuera mayor! Fantástica información sobre los objetos de diseño de composición, gracias.
Fattie
1
Hola, estoy intentando esto. A pesar de que estoy configurando el diseño en viewdidload como lo hizo anteriormente, recibo el bloqueo que indica que la vista de colección debe inicializarse con un parámetro de diseño que no sea nulo. ¿Se me escapa algo? Esto es Xcode 11 para iOS 13.
geistmate
29

Problema

Está buscando una altura automática y también desea tener un ancho completo, no es posible obtener ambos en el uso UICollectionViewFlowLayoutAutomaticSize.

Lo que desea hacer es usar UICollectionViewlo que a continuación es la solución para usted.

Solución

Paso I : Calcule la altura esperada de la celda

1. Si sólo tiene UILabel en CollectionViewCellque establecer el numberOfLines=0y que calcula la altura esperada de UIlable, pasar el los tres parametros

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. Si CollectionViewCellsolo contiene UIImageViewy si se supone que es dinámico en altura, entonces necesita obtener la altura de UIImage ( UIImageViewdebe tener AspectRatiorestricciones)

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

3. Si contiene ambos, calcule su altura y súmelos.

PASO II : Devuelva el tamaño deCollectionViewCell

1. Agregue UICollectionViewDelegateFlowLayoutdelegado en su viewController

2. Implementar el 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)
}

Espero que esto lo pueda ayudar.

Dhiru
fuente
1
¿Es posible tener un UICollectionView de desplazamiento horizontal con la altura de las celdas aumentando verticalmente con el diseño automático?
nr5
Quiero lograr el mismo pero medio ancho y altura dinámica
Mihir Mehta
return CGSize(width: view.frame.width/2, height: expectedHeight)También tenga en cuenta el relleno que devolver el ancho, de lo contrario, dos celdas no cabrían en una sola fila.
Dhiru
@ nr5 ¿Obtuviste la solución? Mi requisito es el mismo que el tuyo. Gracias.
Maulik Bhuptani
@MaulikBhuptani No pude hacerlo completamente con Auto Layout. Tuve que crear una clase de diseño personalizada y anular algunos métodos de clase.
nr5
18

Hay dos formas de abordar este problema.

Una forma es darle al diseño de flujo de la vista de colección un tamaño estimado y calcular el tamaño de celda.

Nota: Como se menciona en los comentarios a continuación, a partir de iOS 10 ya no es necesario proporcionar un tamaño estimado para activar la llamada a una celda func preferredLayoutAttributesFitting(_ layoutAttributes:). Anteriormente (iOS 9) requería que proporcionara un tamaño estimado si deseaba consultar una celda prefferedLayoutAttributes.

(asumiendo que está utilizando guiones gráficos y la vista de colección está conectada a través de IB)

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

Para celdas simples, eso suele ser suficiente. Si el tamaño sigue siendo incorrecto, en la celda de la vista de colección puede anular func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, lo que le dará un mayor control sobre el tamaño de la celda. Nota: aún deberá darle al diseño de flujo un tamaño estimado .

Luego anule func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributespara devolver el tamaño correcto.

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, en su lugar, puede usar una celda de tamaño para calcular el tamaño de la celda en el 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)
}

Si bien estos no son ejemplos perfectos listos para la producción, deberían ayudarlo a comenzar en la dirección correcta. No puedo decir que esta sea la mejor práctica, pero me funciona, incluso con celdas bastante complejas que contienen varias etiquetas, que pueden o no ajustarse a varias líneas.

Eric Murphey
fuente
@Fattie No está claro cómo 'esitmatedItemSize' es irrelevante para una celda de vista de colección de tamaño dinámico, por lo que me gustaría escuchar sus pensamientos. (no sarcasmo)
Eric Murphey
Además, si a esta respuesta le falta algo específico para ser útil y / o lo que considera canónico, hágamelo saber. No discutiré la recompensa, solo me gustaría ayudar a cualquier otra persona que vea esta pregunta.
Eric Murphey
1
(Incluir o no calcular el tamaño del elemento estimado no hace ninguna diferencia en el iOS actual, y no es de ninguna ayuda, de cualquier manera, en el problema de "ancho completo + altura dinámica".) el código al final, las puntas rara vez se usan en estos días y tiene poca relevancia para una altura dinámica (de, por ejemplo, algunas vistas de texto de altura dinámica). gracias sin embargo
Fattie
También llegué a esta solución, pero en mi caso, iOS 11, Xcode 9.2, el contenido de la celda parecía desconectado de la celda en sí; no pude obtener una imagen para expandirse a la altura o el ancho completos de la dimensión fija. Además, noté que CV tenía restricciones de configuración en preferredLayoutAttributesFitting. Luego agregué una restricción propia en dicha función, uniéndome a la dimensión fija de EstimatedItemSize, FWIW, vea mi respuesta a continuación.
Chris Conover
16

Encontré una solución bastante fácil para ese problema: dentro de mi CollectionViewCell obtuve un UIView () que en realidad es solo un fondo. Para obtener el ancho completo, simplemente establezco los siguientes anclajes

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

La "magia" ocurre en la primera línea. Configuré el widthAnchor dinámicamente al ancho de la pantalla. También es importante restar las inserciones de su CollectionView. De lo contrario, la celda no aparecerá. Si no desea tener esa vista de fondo, simplemente hágalo invisible.

FlowLayout utiliza la siguiente configuración

layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

El resultado es una celda de tamaño de ancho completo con altura dinámica.

ingrese la descripción de la imagen aquí

inf1783
fuente
1
vaca sagrada - ¡eso es asombroso! tan obvio ! brillante!
Fattie
1
solo TBC @ inf1783, ¿estás haciendo esto en UICollectionView? (no vista de tabla) - ¿correcto?
Fattie
1
@Fattie Sí, es un UICollectionView;)
inf1783
fantástico @ inf1783, no puedo esperar para probar esto. Por cierto, otra buena manera de hacerlo es subclasificar la UIView y simplemente agregar un ancho intrínseco (supongo que tendría el mismo efecto, y probablemente también lo haría funcionar en el momento del guión gráfico)
Fattie
1
Cada vez que intento esto, termino en un bucle sin fin con el error El comportamiento del UICollectionViewFlowLayout no está definido: /
Skodik.o
7

¡¡¡TRABAJANDO!!! Probado en IOS: 12.1 Swift 4.1

Tengo una solución muy simple que simplemente funciona sin romper restricciones.

ingrese la descripción de la imagen aquí

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


}

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

}

El ingrediente principal es el systemLayoutSizeFitting función en Customcell. Y también tenemos que establecer el ancho de la vista dentro de la celda con restricciones.

Abhishek Biswas
fuente
6

Tienes que agregar restricción de ancho a CollectionViewCell

class SelfSizingCell: UICollectionViewCell {

  override func awakeFromNib() {
      super.awakeFromNib()
      contentView.translatesAutoresizingMaskIntoConstraints = false
      contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
  }
}
Mecid
fuente
finalmente alguien lo hizo perfecto en dos líneas
Omar N Shamali
Esto limitará la celda a un ancho fijo, difícilmente de tamaño propio. Si tuviera que rotar el dispositivo o ajustar el tamaño de la pantalla en un iPad, permanecerá fijo.
Thomas Verbeek
4
  1. Conjunto estimatedItemSizede su diseño de flujo:

    collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
  2. Defina una restricción de ancho en la celda y configúrela para que sea igual al ancho de la supervista:

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

Ejemplo completo

Probado en iOS 11.

romano
fuente
3

Personalmente, encontré las mejores formas de tener un UICollectionView donde AutoLayout determina el tamaño mientras que cada celda puede tener un tamaño diferente es implementar la función UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath mientras se usa una celda real para medir el tamaño.

Hablé de esto en una de las publicaciones de mi blog.

Con suerte, este te ayudará a lograr lo que deseas. No estoy 100% seguro, pero creo que, a diferencia de UITableView, donde en realidad puede tener una altura de celdas completamente automática usando AutoLayout en combinación con

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44

UICollectionView no tiene una forma de permitir que AutoLayout determine el tamaño porque UICollectionViewCell no necesariamente llena todo el ancho de la pantalla.

Pero aquí hay una pregunta para usted : si necesita celdas de ancho de pantalla completo, ¿por qué se molesta en usar UICollectionView sobre un UITableView antiguo que viene con las celdas de tamaño automático?

xxtesaxx
fuente
manténgalo, Aprit a continuación dice que UICollectionViewFlowLayoutAutomaticSize ahora está disponible en diseño de flujo, en iOS10 ...
Fattie
También acabo de ver esto. Aparentemente, eso es algo ahora en iOS 10. Acabo de ver la charla de la WWDC correspondiente sobre esto: developer.apple.com/videos/play/wwdc2016/219 Sin embargo, esto automáticamente ajustará el tamaño de la celda para que se ajuste a su contenido. No estoy seguro de cómo podría decirle a la celda (con AutoLayout) que llene el ancho de la pantalla, ya que no puede configurar una restricción entre UICollectionViewCell y su padre (al menos no en StoryBoards)
xxtesaxx
Derecha. parece que no estamos más cerca de una solución real a este problema increíblemente obvio. : /
Fattie
Según la charla, aún podría sobrescribir sizeThatFits () o favoriteLayoutAttributesFitting () y calcular el tamaño usted mismo, pero creo que eso no es lo que pidió el OP. De todos modos, todavía estaría interesado en el caso de uso para cuando él / ella necesite un UICollectionViewCell de ancho completo y por qué sería tan importante no usar un UITableView en este caso particular (seguro que puede haber ciertos casos, pero supongo que solo tienes que lidiar con hacer algunos cálculos tú mismo)
xxtesaxx
Pensándolo bien, es bastante increíble que no pueda simplemente establecer "columnas == 1" (y luego quizás columnas == 2 cuando el dispositivo está de lado), con vistas de colección.
Fattie
3

Según mi comentario sobre la respuesta de Eric, mi solución es muy similar a la suya, pero tuve que agregar una restricción en favoriteSizeFor ... para restringir a la dimensión fija.

    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 pregunta tiene varios duplicados, la respondí en detalle aquí y proporcioné una aplicación de muestra funcional aquí.

Chris Conover
fuente
2

No estoy seguro si esto califica como una "muy buena respuesta", pero es lo que estoy usando para lograrlo. Mi diseño de flujo es horizontal y estoy tratando de ajustar el ancho con el diseño automático, por lo que es similar a su situación.

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

Luego, en el controlador de vista donde se modifica la restricción de diseño automático, disparo una NSNotification.

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

En mi subclase UICollectionView, escucho esa notificación:

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

e invalidar el diseño:

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

Esto hace sizeForItemAtque se vuelva a llamar utilizando el nuevo tamaño de la vista de colección. En su caso, debería poder actualizarse dadas las nuevas restricciones disponibles en el diseño.

Mark Suman
fuente
solo TBC Mark, ¿quieres decir que en realidad estás cambiando el tamaño de una celda? (es decir, aparece la celda y es de tamaño X, luego algo sucede en su aplicación y se convierte en tamaño Y ..?) gracias
Fattie
Si. Cuando el usuario arrastra un elemento en la pantalla, se activa un evento que actualiza el tamaño de la celda.
Mark Suman
1

En su viewDidLayoutSubviews, establezca el estimatedItemSizeancho completo (el diseño se refiere al objeto UICollectionViewFlowLayout):

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

En su celda, asegúrese de que sus restricciones toquen tanto la parte superior como la inferior de la celda (el siguiente código usa Cartografía para simplificar la configuración de las restricciones, pero puede hacerlo con NSLayoutConstraint o IB si lo desea):

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, sus células ahora crecerán automáticamente en altura!

Raphael
fuente
0

Ninguna de las soluciones me funcionó, ya que necesito un ancho dinámico para adaptarme al ancho de los 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()
        }
    }
Flujo de iOS
fuente
0

Desde iOS 10, tenemos una nueva API en diseño de flujo para hacer eso.

Todo lo que tiene que hacer es configurar tu flowLayout.estimatedItemSizea una nueva constante, UICollectionViewFlowLayoutAutomaticSize.

Fuente

Arpit Dongre
fuente
Hmm, estás bastante seguro de que eso lo hace * ancho completo ? Entonces, ¿esencialmente una columna?
Fattie
Podría hacerlo si establece el ancho de UICollectionViewCell en el ancho completo explícitamente.
Arpit Dongre
No, no fija el ancho a la altura completa e ignora el valor de tamaño estimado a menos que siga la respuesta de Eric arriba, o la mía abajo / más tarde.
Chris Conover
De acuerdo, desafortunadamente, ¡esto no tiene absolutamente ninguna conexión con el problema! je! : O
Fattie
0

AutoLayout se puede utilizar para ajustar automáticamente el tamaño de las celdas en CollectionView en 2 sencillos pasos:

  1. Habilitación del tamaño de celda dinámico

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

  1. Tenga una vista de contenedor y configure el contenedorView.widthAnchor.constraint de collectionView(:cellForItemAt:)para limitar el ancho de contentView al ancho 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
        }
    }

    ....
}

Eso es todo, obtendrás el resultado deseado. Consulte las siguientes esencias para obtener el código completo:

Referencia / Créditos:

Captura de pantalla: ingrese la descripción de la imagen aquí

JolasJoe
fuente
-6

Tienes que heredar la clase UICollectionViewDelegateFlowLayout en tu collectionViewController. Luego agrega la función:

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

Con eso, tiene el tamaño de ancho del ancho de la pantalla.

Y ahora tiene un collectionViewController con filas como tableViewController.

Si desea que el tamaño de la altura de cada celda sea dinámicamente, tal vez debería crear celdas personalizadas para cada celda que necesite.

Edu
fuente
7
Ésta no es exactamente la respuesta a la pregunta :) Eso daría una altura fija de 100, es decir, así es como existía uno programado antes del diseño automático.
Fattie