UICollectionView, cellules pleine largeur, autorise la mise en page automatique de la hauteur dynamique?


120

Dans une verticale UICollectionView,

Est-il possible d'avoir des cellules pleine largeur , mais autoriser le contrôle de la hauteur dynamique par mise en page automatique ?

Cela me semble peut-être la "question la plus importante dans iOS sans vraiment bonne réponse."


Important:

Notez que dans 99% des cas, pour obtenir des cellules pleine largeur + une hauteur dynamique de mise en page automatique, utilisez simplement une vue de tableau. C'est si facile.


Alors, quel est un exemple où vous avez besoin d'une vue de collection?

Les vues de collection sont incroyablement plus puissantes que les vues de table.

Un exemple simple où vous réellement devez utiliser une vue de la collecte, pour obtenir des cellules en pleine largeur + hauteur de dynamique autolayout:

Si vous animez entre deux mises en page dans une vue de collection. Par exemple, entre une disposition à 1 et 2 colonnes, lorsque l'appareil pivote.

C'est un idiome courant et normal dans iOS. Malheureusement, cela ne peut être réalisé qu'en résolvant le problème posé dans ce contrôle qualité. : - /


oui est possible avec la classe UICollectionViewDelegateFlowLayout
Sunil Prajapati

cela ne signifie-t-il pas que vous pourriez réellement utiliser une tableView? quelle fonctionnalité supplémentaire serait nécessaire pour vous, pour compliquer cela?
Catalina T.

1
Salut @CatalinaT. , les vues de collection ont de très nombreuses fonctionnalités supplémentaires par rapport aux vues de table. Un exemple simple est l'ajout de physique. (Si vous ne savez pas ce que je veux dire, c'est ainsi que vous créez les listes "rebondissantes" dans iOS.)
Fattie

vous pouvez également appliquer en quelque sorte la physique (comme SpringDampinget initialSpringVelocity) dans le tableau Cell ..... jetez un œil à cette liste gonflable en utilisant tableView ,,,, pastebin.com/pFRQhkrX vous pouvez animer le TableViewCelltableau dans la willDisplayCell:(UITableViewCell *)cellméthode delgate @Fattie vous pouvez accepter la réponse qui a résolu votre problème pour aider les autres qui recherchent le même problème
Dhiru

ohk, avez-vous essayé la liste Bouncy en utilisant la vue par liste? @Fattie
Dhiru

Réponses:


124

1. Solution pour iOS 13+

Avec Swift 5.1 et iOS 13, vous pouvez utiliser des objets de mise en page compositionnelle pour résoudre votre problème.

L'exemple de code complet suivant montre comment afficher la multiligne à l' UILabelintérieur de la pleine largeur 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")
    }

}

Affichage attendu:

entrez la description de l'image ici


2. Solution pour iOS 11+

Avec Swift 5.1 et iOS 11, vous pouvez sous UICollectionViewFlowLayout- classer et définir sa estimatedItemSizepropriété sur UICollectionViewFlowLayout.automaticSize(cela indique au système que vous souhaitez gérer le redimensionnement automatique UICollectionViewCell). Vous devrez ensuite remplacer layoutAttributesForElements(in:)et layoutAttributesForItem(at:)pour définir la largeur des cellules. Enfin, vous devrez remplacer la preferredLayoutAttributesFitting(_:)méthode de votre cellule et calculer sa hauteur.

Le code complet suivant montre comment afficher une multiligne à l' UILabelintérieur de la pleine largeur UIcollectionViewCell(contrainte par UICollectionViewla zone de sécurité de et UICollectionViewFlowLayoutles inserts 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
    }

}

Voici quelques implémentations alternatives pour 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
}

Affichage attendu:

entrez la description de l'image ici


Cela a bien fonctionné pour moi jusqu'à ce que j'aie ajouté des en-têtes de section. Les en-têtes de section sont mal placés et couvrent d'autres cellules dans iOS 11. Mais fonctionne bien dans iOS 12. Avez-vous rencontré un problème avec l'ajout d'en-têtes de section?
Nakul

Merci beaucoup. Tu as sauvé ma semaine. 🙇‍♂️
Gaetan

1
CollectionView défile vers le haut chaque fois que la taille change
Sajith

1
fantastique nouvelle, @ImanouPetit! Je souhaite que la prime soit plus grande! Des informations fantastiques sur les objets de disposition de composition, merci.
Fattie le

1
Salut, j'essaye ça. Même si je configure la mise en page dans viewdidload comme vous l'avez fait ci-dessus, j'obtiens le plantage indiquant que la collectionview doit être initialisée avec un paramètre de mise en page non nul. Y a-t-il quelque chose qui me manque? Ceci est Xcode 11 pour iOS 13.
geistmate

29

Problème

Vous recherchez une hauteur automatique et souhaitez également avoir une largeur totale, il n'est pas possible d'obtenir les deux en utilisant UICollectionViewFlowLayoutAutomaticSize.

Vous voulez faire en utilisant UICollectionViewdonc ci-dessous est la solution pour vous.

Solution

Étape I : Calculez la hauteur attendue de la cellule

1. Si vous n'avez que UILabel dans CollectionViewCellque défini le numberOfLines=0et qui a calculé la hauteur attendue de UIlable, passez les trois paramètres

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 votre CollectionViewCellcontient uniquement UIImageViewet s'il est censé être dynamique en hauteur, vous devez obtenir la hauteur de UIImage (vous UIImageViewdevez avoir des AspectRatiocontraintes)

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

3. S'il contient les deux, calculez leur hauteur et ajoutez-les ensemble.

ÉTAPE-II : Renvoyez la taille deCollectionViewCell

1. Ajouter un UICollectionViewDelegateFlowLayoutdélégué dans votre viewController

2. Implémentez la méthode déléguée

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

J'espère que cela vous aidera.


1
Est-il possible d'avoir un UICollectionView à défilement horizontal avec une hauteur de cellules augmentant verticalement avec la disposition automatique?
nr5

Je veux accomplir la même mais demi-largeur et hauteur dynamique
Mihir Mehta

return CGSize(width: view.frame.width/2, height: expectedHeight)gardez également le remplissage en compte plutôt que de renvoyer la largeur, sinon deux cellules ne rentreraient pas dans une seule ligne.
Dhiru le

@ nr5 Avez-vous trouvé la solution? Mon exigence est la même que la vôtre. Merci.
Maulik Bhuptani

@MaulikBhuptani Je ne pouvais pas le faire complètement avec Auto Layout. J'ai dû créer une classe Layout personnalisée et remplacer certaines méthodes de classe.
nr5

18

Il existe plusieurs façons de résoudre ce problème.

Une façon est de donner à la disposition de flux de vue de collection une taille estimée et de calculer la taille de cellule.

Remarque: comme mentionné dans les commentaires ci-dessous, à partir d'iOS 10, vous n'avez plus besoin de fournir une taille estimée pour déclencher l'appel à une cellule func preferredLayoutAttributesFitting(_ layoutAttributes:). Auparavant (iOS 9) vous obligeait à fournir une taille estimée si vous vouliez interroger une cellule prefferedLayoutAttributes.

(en supposant que vous utilisez des storyboards et que la vue de la collection soit connectée via IB)

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

Pour des cellules simples, cela suffira généralement. Si la taille est toujours incorrecte, dans la cellule de vue de collection, vous pouvez remplacer func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, ce qui vous donnera un contrôle plus fin sur la taille de la cellule. Remarque: vous devrez toujours donner à la disposition de flux une taille estimée .

Remplacez ensuite func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributespour renvoyer la taille correcte.

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
}

Vous pouvez également utiliser une cellule de dimensionnement pour calculer la taille de la cellule dans le 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)
}

Bien qu'il ne s'agisse pas d'exemples parfaits pour la production, ils devraient vous aider à démarrer dans la bonne direction. Je ne peux pas dire que ce soit la meilleure pratique, mais cela fonctionne pour moi, même avec des cellules assez complexes contenant plusieurs étiquettes, qui peuvent ou non envelopper plusieurs lignes.


@Fattie On ne sait pas en quoi «esitmatedItemSize» n'est pas pertinent pour une cellule de vue de collection de taille dynamique, alors j'aimerais entendre vos pensées. (not sarcasm)
Eric Murphey

De plus, si cette réponse manque quelque chose de spécifique pour être utile et / ou ce que vous considérez comme canonique, faites-le moi savoir. Je ne discuterai pas de la prime, je voudrais simplement être utile à quiconque regarde cette question.
Eric Murphey

1
(inclure ou non estiméItemSize ne fait aucune différence dans iOS actuel, et n'aide pas du tout, dans les deux cas, dans le problème "pleine largeur + hauteur dynamique".) le code à la fin, les pointes sont rarement utilisées de nos jours et il a peu de pertinence pour une hauteur dynamique (à partir, par exemple, de quelques vues de texte de hauteur dynamique). merci bien
Fattie

Je suis également arrivé à cette solution, mais dans mon cas, iOS 11, Xcode 9.2, le contenu de la cellule semblait déconnecté de la cellule elle-même - je ne pouvais pas obtenir une image qui s'étende sur toute la hauteur ou la largeur de la dimension fixe. J'ai en outre remarqué que CV était des contraintes de paramètres dans preferredLayoutAttributesFitting. J'ai ensuite ajouté une contrainte de ma part dans ladite fonction, se liant à la dimension fixe de sharedItemSize, FWIW, voir ma réponse ci-dessous.
Chris Conover

16

J'ai trouvé une solution assez simple à ce problème: à l'intérieur de ma CollectionViewCell, j'ai un UIView () qui n'est en fait qu'un arrière-plan. Pour obtenir la pleine largeur, je viens de définir les ancres suivantes

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 "magie" se produit dans la première ligne. J'ai défini dynamiquement widthAnchor sur la largeur de l'écran. Il est également important de soustraire les encarts de votre CollectionView. Sinon, la cellule n'apparaîtra pas. Si vous ne voulez pas avoir une telle vue d'arrière-plan, rendez-la simplement invisible.

Le FlowLayout utilise les paramètres suivants

layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

Le résultat est une cellule pleine largeur avec une hauteur dynamique.

entrez la description de l'image ici


1
vache sacrée - c'est incroyable! tellement évident ! brillant !
Fattie

1
juste TBC @ inf1783, vous faites cela dans UICollectionView? (pas en vue de la table) - correct?
Fattie

1
@Fattie Oui, c'est un UICollectionView;)
inf1783

fantastique @ inf1783, j'ai hâte d'essayer ça. À propos, un autre bon moyen de le faire est de sous-classer l'UIView et d'ajouter simplement une largeur intrinsèque (je suppose que cela aurait le même effet, et cela le ferait probablement fonctionner au moment du storyboard également)
Fattie

1
Chaque fois que j'essaye cela, je me retrouve dans une boucle sans fin avec l'erreur Le comportement de l'UICollectionViewFlowLayout n'est pas définie: /
Skodik.o

7

TRAVAIL!!! Testé sur IOS: 12.1 Swift 4.1

J'ai une solution très simple qui fonctionne simplement sans rupture de contrainte.

entrez la description de l'image ici

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

}

Ingrédient principal est la systemLayoutSizeFitting fonction Customcell. Et nous devons également définir la largeur de la vue à l'intérieur de Cell avec des contraintes.


6

Vous devez ajouter une contrainte de largeur à CollectionViewCell

class SelfSizingCell: UICollectionViewCell {

  override func awakeFromNib() {
      super.awakeFromNib()
      contentView.translatesAutoresizingMaskIntoConstraints = false
      contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
  }
}

enfin quelqu'un l'a rendu parfait en deux lignes
Omar N Shamali

Cela limitera la cellule à une largeur fixe, difficilement auto-dimensionnante. Si vous deviez faire pivoter l'appareil ou ajuster la taille de l'écran sur un iPad, cela restera fixe.
Thomas Verbeek

4
  1. Ensemble estimatedItemSizede votre disposition de flux:

    collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
  2. Définissez une contrainte de largeur dans la cellule et définissez-la pour qu'elle soit égale à la largeur de superview:

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

Exemple complet

Testé sur iOS 11.


3

Personnellement, j'ai trouvé le meilleur moyen d'avoir un UICollectionView où AutoLayout détermine la taille tandis que chaque cellule peut avoir une taille différente est d'implémenter la fonction UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath tout en utilisant une cellule réelle pour mesurer la taille.

J'en ai parlé dans l'un de mes articles de blog

Espérons que celui-ci vous aidera à réaliser ce que vous voulez. Je ne suis pas sûr à 100% mais je crois que contrairement à UITableView où vous pouvez en fait avoir une hauteur de cellules entièrement automatique en utilisant AutoLayout en conjonction avec

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44

UICollectionView n'a pas une telle façon de laisser AutoLayout déterminer la taille car UICollectionViewCell ne remplit pas nécessairement toute la largeur de l'écran.

Mais voici une question pour vous : si vous avez besoin de cellules de pleine largeur d'écran, pourquoi vous embêtez-vous même à utiliser UICollectionView sur un bon vieux UITableView qui est livré avec les cellules de dimensionnement automatique?


tenez-le, Aprit ci-dessous dit que UICollectionViewFlowLayoutAutomaticSize est maintenant disponible en disposition de flux, dans iOS10 ...
Fattie

J'ai aussi juste vu ça. Apparemment, c'est une chose maintenant dans iOS 10. Je viens de regarder le discours WWDC correspondant à ce sujet: developer.apple.com/videos/play/wwdc2016/219 Cependant, cela va complètement auto-dimensionner la cellule pour l'adapter à son contenu. Je ne sais pas comment vous pourriez dire à la cellule (avec AutoLayout) de remplir la largeur de l'écran car vous ne pouvez pas configurer une contrainte entre UICollectionViewCell et son parent (du moins pas dans StoryBoards)
xxtesaxx

droite. nous ne semblons pas plus près d'une solution réelle à ce problème incroyablement évident. : /
Fattie

Selon la conférence, vous pouvez toujours écraser sizeThatFits () ou preferLayoutAttributesFitting () et calculer la taille par vous-même, mais ce n'est pas ce que l'OP a demandé, je pense. Quoi qu'il en soit, je serais toujours intéressé par le cas d'utilisation du moment où il / elle a besoin d'un UICollectionViewCell pleine largeur et pourquoi ce serait si important de ne pas utiliser un UITableView dans ce cas particulier (bien sûr, il peut y avoir certains cas, mais alors je suppose vous avez juste à faire quelques calculs vous-même)
xxtesaxx

en y réfléchissant, il est assez incroyable que vous ne puissiez pas simplement définir "colonnes == 1" (et peut-être ensuite colonnes == 2 lorsque l'appareil est sur le côté), avec des vues de collection.
Fattie

3

D'après mon commentaire sur la réponse d'Eric, ma solution est très similaire à la sienne, mais j'ai dû ajouter une contrainte dans favoriteSizeFor ... afin de contraindre à la dimension fixe.

    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
    }

Cette question a un certain nombre de doublons, j'y ai répondu en détail ici et fourni un exemple d'application fonctionnel ici.


2

Je ne sais pas si cela se qualifie comme une "très bonne réponse", mais c'est ce que j'utilise pour y parvenir. Ma disposition de flux est horizontale et j'essaie d'ajuster la largeur avec la disposition automatique, donc c'est similaire à votre situation.

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

Ensuite, dans le contrôleur de vue où la contrainte de mise en page automatique est modifiée, je déclenche une NSNotification.

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

Dans ma sous-classe UICollectionView, j'écoute cette notification:

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

et invalider la mise en page:

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

Cela provoque sizeForItemAtle nouvel appel en utilisant la nouvelle taille de la vue de collection. Dans votre cas, il devrait pouvoir se mettre à jour compte tenu des nouvelles contraintes disponibles dans la mise en page.


juste TBC Mark voulez-vous dire que vous changez réellement la taille d'une cellule? (c'est-à-dire, la cellule apparaît et elle est de taille X, puis quelque chose se passe dans votre application et elle devient de taille Y ..?) thx
Fattie

Oui. Lorsque l'utilisateur fait glisser un élément sur l'écran, un événement se déclenche qui met à jour la taille de la cellule.
Mark Suman

1

Sur votre viewDidLayoutSubviews, définissez la estimatedItemSizesur pleine largeur (la disposition fait référence à l'objet UICollectionViewFlowLayout):

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

Sur votre cellule, assurez-vous que vos contraintes touchent à la fois le haut et le bas de la cellule (le code suivant utilise la cartographie pour simplifier la définition des contraintes mais vous pouvez le faire avec NSLayoutConstraint ou IB si vous le souhaitez):

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, vos cellules vont maintenant croître automatiquement en hauteur!


0

Aucune des solutions ne fonctionnait pour moi car j'ai besoin d'une largeur dynamique pour s'adapter à la largeur des 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()
        }
    }

0

Depuis iOS 10, nous avons une nouvelle API sur la disposition des flux pour ce faire.

Tout ce que vous devez faire est de définir votre flowLayout.estimatedItemSizeà une nouvelle constante, UICollectionViewFlowLayoutAutomaticSize.

La source


Hmm, vous êtes à peu près sûr que cela le rend * pleine largeur ? Donc, essentiellement une colonne?
Fattie

Cela peut se produire si vous définissez explicitement la largeur de UICollectionViewCell sur pleine largeur.
Arpit Dongre

Non, il ne fixe pas la largeur à la hauteur totale et ignore la valeur de taille estimée à moins que vous ne suiviez la réponse d'Eric ci-dessus, ou la mienne ci-dessous / plus tard.
Chris Conover

OK, malheureusement, cela n'a absolument aucun lien avec le problème! il h! : O
Fattie

0

La mise en page automatique peut être utilisée pour le dimensionnement automatique des cellules dans CollectionView en 2 étapes faciles:

  1. Activation du dimensionnement dynamique des cellules

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

  1. Avoir une vue de conteneur et définir le containerView.widthAnchor.constraint de collectionView(:cellForItemAt:)pour limiter la largeur de contentView à la largeur 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
        }
    }

    ....
}

Voilà, vous obtiendrez le résultat souhaité. Reportez-vous aux éléments essentiels suivants pour le code complet:

Référence / Crédits:

Capture d'écran: entrez la description de l'image ici


-6

Vous devez hériter de la classe UICollectionViewDelegateFlowLayout sur votre collectionViewController. Ajoutez ensuite la fonction:

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

En utilisant cela, vous avez la taille de largeur de la largeur de l'écran.

Et vous avez maintenant un collectionViewController avec des lignes en tant que tableViewController.

Si vous souhaitez que la taille de la hauteur de chaque cellule soit dynamique, vous devriez peut-être créer des cellules personnalisées pour chaque cellule dont vous avez besoin.


7
Ce n'est pas exactement la réponse à la question :) Cela donnerait une hauteur fixe de 100, c'est-à-dire que c'était comme ça qu'on était programmé avant même que l'autolayout existe.
Fattie
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.