182 votes

UICollectionView, cellules pleine largeur, autorisent la hauteur dynamique autolayout?

Voici un assez question de base sur la situation actuelle de développement iOS, à qui je n'ai jamais trouvé une bonne réponse.


Dans une (dis) verticales UICollectionView ,

Est-il possible d'avoir toute la largeur des cellules, mais, de permettre à la dynamique de la hauteur pour être contrôlée par la mise en page automatique?

(Si vous êtes nouveau à iOS "dynamique de la hauteur", ce qui signifie que la cellule a quelques, dire le texte des points de vue qui pourrait être n'importe quelle longueur ou des images qui pourraient être de tailles différentes, de sorte que, finalement, chaque cellule est totalement différente d'une hauteur.)

Si oui, comment?

Ce qui me frappe, comme peut-être la "question la plus importante dans iOS avec pas vraiment de bonne réponse."

231voto

Imanou Petit Points 43068

Avec Swift 4.2 et iOS 12, vous pouvez sous-classe UICollectionViewFlowLayout et l'ensemble de ses estimatedItemSize de la propriété d' UICollectionViewFlowLayoutAutomaticSize (ce qui indique au système que vous voulez traiter d'un redimensionnement automatique UICollectionViewCells). Vous devrez alors remplacer layoutAttributesForElements(in:) et layoutAttributesForItem(at:) , afin de définir des cellules de largeur. Enfin, vous aurez à remplacer les cellules de votre preferredLayoutAttributesFitting(_:) méthode et de calculer leurs comprimé hauteur d'ajustement.


La suite complète de code suivant montre comment afficher multiligne UILabel à l'intérieur de pleine largeur en UIcollectionViewCell (contraint par UICollectionView's de la zone de sécurité et d' UICollectionViewFlowLayouts'encarts):

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let items = [
        "Lorem ipsum.",
        "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. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
        "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."
    ]
    let columnLayout = FlowLayout()

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.alwaysBounceVertical = true
        collectionView.collectionViewLayout = columnLayout
        collectionView.contentInsetAdjustmentBehavior = .always
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
    }

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

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

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    override init() {
        super.init()

        self.minimumInteritemSpacing = 10
        self.minimumLineSpacing = 10
        self.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    }

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

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath) else { return nil }
        guard let collectionView = collectionView else { return nil }
        layoutAttributes.bounds.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right
        return layoutAttributes
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let superLayoutAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
        guard scrollDirection == .vertical else { return superLayoutAttributes }

        let computedAttributes = superLayoutAttributes.compactMap { layoutAttribute in
            return layoutAttribute.representedElementCategory == .cell ? layoutAttributesForItem(at: layoutAttribute.indexPath) : layoutAttribute
        }
        return computedAttributes
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

}

Cellule.swift

import UIKit

class Cell: 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.layoutMarginsGuide.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
    }

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

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

}

Voici quelques implémentations alternatives pour preferredLayoutAttributesFitting(_:):

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutIfNeeded()
    label.preferredMaxLayoutWidth = label.bounds.size.width
    layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
    return layoutAttributes
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    label.preferredMaxLayoutWidth = layoutAttributes.size.width - contentView.layoutMargins.left - contentView.layoutMargins.left
    layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
    return layoutAttributes
}

Devrait afficher:

enter image description here

32voto

Dhiru Points 1943

Problème

Vous êtes à la recherche pour le réglage automatique de la hauteur et qui veulent aussi avoir la pleine largeur, il n'est pas possible d'avoir les deux à l'aide de UICollectionViewFlowLayoutAutomaticSize. pourquoi vous ne prenez pas d' UITableVIew au lieu de UICollectionView dans lequel vous pouvez simplement retourner dans l' heightForRow comme UITableViewAutomaticDimension il sera géré automatiquement. De toute façon que vous voulez faire à l'aide de UICollectionView ce qui est en bas est la solution pour vous.

Solution

Étape I: Calculer la hauteur prévue de la Cellule

1. Si vous n'avez qu' UILabel en CollectionViewCell que l' numberOfLines=0 et celle calculée à la hauteur prévue de l' UIlable,- passer tous 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 CollectionViewCell ne contient qu' UIImageView et si c'est censé être dynamique dans la Hauteur que vous avez besoin pour obtenir la hauteur de l' UIImage (votre UIImageView devez disposer AspectRatio contraintes)

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

3. Si elle contient à la fois de celles calculées leur hauteur et ajoutez-les ensemble.

ÉTAPE II: de Retour de la Taille de l' CollectionViewCell

1. Ajouter UICollectionViewDelegateFlowLayout délégué dans votre viewController

2. Mettre en œuvre la méthode du délégué

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 va vous aider.

23voto

Eric Murphey Points 1216

Il existe quelques façons dont vous pourriez faire face à ce problème.

Une façon est que vous pouvez donner à l'affichage de la collection la disposition de flux une estimation de la taille et de calcul de la taille de la cellule.

Remarque: Comme indiqué dans les commentaires ci-dessous, comme d'iOS 10, vous n'avez plus à fournir et l'estimation de la taille de déclencher l'appel à une des cellules func preferredLayoutAttributesFitting(_ layoutAttributes:). Précédemment (iOS 9) serait vous demander de fournir une estimation de la taille si vous voulais requête d'une des cellules prefferedLayoutAttributes.

(en supposant que vous utilisez des story-boards et l'affichage de la collection est connecté par l'intermédiaire de l'IB)

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

Pour de simples cellules qui sera généralement suffisant. Si la taille est toujours incorrect, dans la vue de collection cellule, vous pouvez remplacer func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, qui vous donnera plus de finesse de grain de contrôle sur la taille de la cellule. Remarque: Vous aurez besoin de donner à la disposition de flux une estimation de la taille.

Ensuite remplacer l' func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes de retour de 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
}

Sinon, au lieu de cela, vous pouvez utiliser une taille de cellule pour calculer la taille de la cellule dans l' 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)
}

Alors que ce ne sont pas parfait, prêt pour la production exemples, ils devraient vous aider à démarrer dans la bonne direction. Je ne peux pas dire que c'est la meilleure pratique, mais cela fonctionne pour moi, même avec assez complexe de cellules contenant plusieurs labels, qui peut ou peut ne pas s'étendre sur plusieurs lignes.

17voto

inf1783 Points 626

Je l'ai trouvé assez simple solution de ce problème: à l'Intérieur de mon CollectionViewCell j'ai eu une UIView() qui est en fait juste un arrière-plan. Pour obtenir toute la largeur, je viens de mettre la suite des Ancres

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 passe dans la première ligne. J'ai mis le widthAnchor dynamiquement à la largeur de l'écran. Il est également important de soustraire les encarts de votre CollectionView. Sinon, la cellule ne pas s'afficher. Si vous ne voulez pas d'avoir un arrière-plan de la vue, il suffit de le rendre invisible.

Le FlowLayout utilise les paramètres suivants

layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

Le résultat est d'une largeur totale de la taille de la cellule avec la dynamique de la hauteur.

enter image description here

6voto

Mecid Points 244

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X