UICollectionView automatise les cellules avec mise en page automatique

j'essaie d'obtenir la taille de moi-même UICollectionViewCells travailler avec la mise en page automatique, mais je ne semble pas pouvoir obtenir les cellules pour se dimensionner au contenu. J'ai du mal à comprendre comment la taille de la cellule est mise à jour à partir du contenu de ce qui est à l'intérieur de contentView de la cellule.

Voici la configuration que j'ai essayé:

  • Personnalisé UICollectionViewCell avec un UITextView dans son contentView.
  • Scrolling pour le UITextView est désactivée.
  • la contrainte horizontale de contentView est:" H:|[_textView(320)]", i.e. le UITextView est épinglé à la gauche de la cellule avec une largeur explicite de 320.
  • la contrainte verticale de contentView est: "V:/-0-[_textView]", i.e. le UITextView épinglé au sommet de la cellule.
  • le UITextView a une contrainte de hauteur fixée à une constante que les rapports UITextView adapteront au texte.

voici à quoi il ressemble avec le fond de la cellule rouge, et le fond UITextView Bleu: cell background red, UITextView background blue

j'ai mis le projet avec lequel j'ai joué sur GitHub ici .

154
demandé sur Rishil P. 2014-09-17 20:02:15

13 réponses

mise à jour pour Swift 4

systemLayoutSizeFittingSize renommé en systemLayoutSizeFitting


mise à jour pour iOS 9

après avoir vu ma solution GitHub casser sous iOS 9 j'ai finalement eu le temps d'examiner la question complètement. J'ai maintenant mis à jour le rapport pour inclure plusieurs exemples de différentes configurations pour les cellules auto dimensionnantes. Ma conclusion est que les cellules auto-dimensionnantes sont grandes en théorie, mais pratique. Un mot de prudence lors de la procédure d'auto dimensionnement des cellules.

TL; DR

Vérifier mon projet GitHub


les cellules auto-dimensionnantes ne sont supportées que par une disposition de flux, donc assurez-vous que c'est ce que vous utilisez.

il y a deux choses que vous devez configurer pour que les cellules auto dimensionnées fonctionnent.

1. estimatedItemSize sur UICollectionViewFlowLayout

la disposition de flux deviendra dynamique dans la nature une fois que vous aurez défini la propriété estimatedItemSize .

self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)

2. Ajouter un support pour le calibrage sur votre cellule sous-classe

en 2 saveurs; mise en page automatique ou sur commande de preferredLayoutAttributesFittingAttributes .

créer et configurer des cellules avec mise en page automatique

je ne vais pas à détail à ce sujet comme il ya un brillant SO post sur la configuration des contraintes pour une cellule. Il suffit de se méfier que Xcode 6 a cassé un tas de trucs avec iOS 7 donc, si vous supportez iOS 7, vous aurez besoin de faire des choses comme s'assurer que l'autoresizingMask est mis sur le contentView de la cellule et que les limites du contentView est défini comme les limites de la cellule quand la cellule est chargée (i.e. awakeFromNib ).

les choses que vous devez savoir est que votre cellule a besoin d'être plus sérieusement restreinte qu'une cellule de vue de Table. Par exemple, si vous voulez que votre largeur soit dynamique, alors votre cellule a besoin d'une contrainte de hauteur. De même, si vous voulez que la hauteur soit dynamique, vous aurez besoin d'une contrainte de largeur à votre cellule.

mettre en Œuvre preferredLayoutAttributesFittingAttributes dans votre cellule personnalisé

lorsque cette fonction est appelée votre vue a déjà été configurée avec du contenu (i.e. cellForItem a été appelé). En supposant que vos contraintes ont été correctement définies, vous pourriez avoir une implémentation comme celle-ci:

//forces the system to do one layout pass
var isHeightCalculated: Bool = false

override func preferredLayoutAttributesFittingAttributes(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    //Exhibit A - We need to cache our calculation to prevent a crash.
    if !isHeightCalculated {
        setNeedsLayout()
        layoutIfNeeded()
        let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
        var newFrame = layoutAttributes.frame
        newFrame.size.width = CGFloat(ceilf(Float(size.width)))
        layoutAttributes.frame = newFrame
        isHeightCalculated = true
    }
    return layoutAttributes
}

NOTE sur iOS 9 le comportement a changé un peu qui pourrait causer des accidents sur votre mise en œuvre si vous n'êtes pas prudent (Voir plus ici ). Lorsque vous implémentez preferredLayoutAttributesFittingAttributes , vous devez vous assurer que vous ne modifiez le cadre de vos attributs qu'une seule fois. Si vous ne le faites pas la mise en page, appelez votre mise en œuvre indéfiniment et éventuellement se planter. Une solution est de mettre en cache la taille calculée dans votre cellule et d'invalider ceci chaque fois que vous réutilisez la cellule ou changez son contenu comme je l'ai fait avec la propriété isHeightCalculated .

l'Expérience de votre mise en page

à ce stade, vous devriez avoir des cellules dynamiques "fonctionnelles" dans votre collection. Je n'ai pas encore trouvé la solution out-of-the-box suffisante au cours de mes tests alors n'hésitez pas à commenter si vous avoir. Il semble toujours que UITableView gagne la bataille pour le dimensionnement dynamique IMHO.

mises en garde

soyez très conscient que si vous utilisez des cellules prototypes pour calculer l'estimatedItemSize - cela va casser si votre XIB utilise des classes de taille . La raison en est que lorsque vous chargez votre cellule à partir d'un XIB, sa classe de taille sera configurée avec Undefined . Ce ne sera cassé Que sur iOS 8 et plus car sur iOS 7 la taille la classe va être chargé en fonction de l'appareil (iPad = Régulier, iPhone = Compact-Tout). Vous pouvez soit définir la taille estimatedItemSize sans charger le XIB, ou vous pouvez charger la cellule à partir du XIB, l'ajouter à la collection (cela définira la collecte de trait), effectuer la mise en page, puis le supprimer de la superview. Vous pouvez aussi faire en sorte que votre cellule supplante le getter traitCollection et renvoie les traits appropriés. C'est à vous.

faites - moi savoir si j'ai manqué n'importe quoi, espère que j'ai aidé et bonne chance codage


226
répondu Daniel Galasko 2018-05-26 19:40:28

quelques changements clés à la réponse de Daniel Galasko a réglé tous mes problèmes. Malheureusement, je n'ai pas assez de réputation pour commenter directement (encore).

à l'étape 1, lorsque vous utilisez la mise en page automatique, il vous suffit d'ajouter un uivi monoparental à la cellule. Tout ce qui est dans la cellule doit être un sous-vue du parent. Qui a répondu à tous mes problèmes. Alors que XCode ajoute ceci pour les cellules de visualisation utilisables automatiquement, il ne le fait pas (mais il devrait le faire) pour les cellules de visualisation Uicollection. Selon le docs :

pour configurer l'apparence de votre cellule, ajoutez les vues nécessaires pour présenter le contenu de l'élément de données comme des sous-vues de la vue dans la propriété contentView. N'ajoutez pas directement de subviews à la cellule elle-même.

puis sauter entièrement l'étape 3. Il n'est pas nécessaire.

35
répondu Matt Koala 2018-01-27 06:03:20

dans iOS10 il y a une nouvelle constante appelée UICollectionViewFlowLayoutAutomaticSize , donc à la place:

self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)

vous pouvez utiliser ceci:

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

il a une meilleure performance en particulier lorsque les cellules dans votre vue collection a wid constante

21
répondu pawel221 2017-03-22 10:26:43

Dans iOS 10+ c'est très simple en 2 étapes.

  1. assurez-vous que tous vos contenus de cellules sont placés dans un uivi (ou à l'intérieur d'un descendant D'uivi comme UIStackView qui simplifie autolayout beaucoup). Tout comme avec les cellules de vue Uitable redimensionnement dynamique, toute la hiérarchie de la vue doit avoir des contraintes configurées, du conteneur le plus externe à la vue la plus interne. Cela inclut les contraintes entre les Uicollection viewcell and the immediate childview

  2. instruisez le flowlayout de votre UICollectionView à la taille automatique

    yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
    
17
répondu Marmoy 2017-11-02 07:07:43
  1. Ajouter flowLayout sur viewDidLoad()

    override func viewDidLoad() {
        super.viewDidLoad() 
        if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.estimatedItemSize = CGSize(width: 1, height:1)
        }
    }
    
  2. aussi, définissez un uivi comme mainContainer pour votre cellule et ajoutez toutes les vues nécessaires à l'intérieur de celle-ci.

  3. reportez-vous à ce tutoriel impressionnant pour plus de référence: UICollectionView avec redimensionnement automatique de la cellule à l'aide de mise en forme automatique dans iOS 9 & 10

7
répondu karenms 2017-08-23 04:50:12

après avoir lutté avec cela pendant un certain temps, j'ai remarqué que le redimensionnement ne fonctionne pas pour UITextViews si vous ne désactivez pas le défilement:

let textView = UITextView()
textView.scrollEnabled = false
4
répondu noobular 2016-02-13 00:59:12

j'ai fait une hauteur de cellule dynamique de vue collection. Voici git hub repo .

et, creuser pourquoi preferredLayoutAttributesFittingattributes est appelé plus d'une fois. En réalité, il sera appelé au moins 3 fois.

l'image du journal de bord de la console : enter image description here

1st preferredlayouttattributesfittingattributes :

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c290e0> index path: (<NSIndexPath:    0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 57.5); 

(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);

layoutAttributes.cadre.taille.la hauteur est l'état actuel 57.5 .

2nd Preferred layouttattributesfittingattributes :

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c16370> index path: (<NSIndexPath: 0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); 

(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);

la hauteur de la cellule a changé en 534.5 comme prévu. Mais, la vue de la collection toujours hauteur zéro.

de la 3ème preferredLayoutAttributesFittingattributes :

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa403d516a0> index path: (<NSIndexPath: 0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); 

(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 477);

vous pouvez voir la hauteur de vue de la collection a été changée de 0 à 477 .

le comportement est similaire à handle scroll:

1. Before self-sizing cell

2. Validated self-sizing cell again after other cells recalculated.

3. Did changed self-sizing cell

au début, je pensais que cette méthode n'appelait qu'une fois. Donc j'ai codé comme suit:

CGRect frame = layoutAttributes.frame;
frame.size.height = frame.size.height + self.collectionView.contentSize.height;
UICollectionViewLayoutAttributes* newAttributes = [layoutAttributes copy];
newAttributes.frame = frame;
return newAttributes;

Cette ligne:

frame.size.height = frame.size.height + self.collectionView.contentSize.height;

causera l'appel système boucle infinie et le plantage de L'application.

N'importe quelle taille changée, il validera toutes les cellules' preferredLayoutAttributesFittingattributes encore et encore jusqu'à chaque position des cellules (I. e frames) ne sont plus un changement.

2
répondu Yang Young 2016-08-11 08:50:48

À qui il peut aider,

j'ai eu ce terrible accident si estimatedItemSize était mis. Même si j'ai retourné 0 dans numberOfItemsInSection . Par conséquent, les cellules elles-mêmes et leur disposition automatique n'ont pas été la cause de l'accident... Le collectionView s'est écrasé, même vide, juste parce que estimatedItemSize était réglé pour l'auto-dimensionnement.

dans mon cas j'ai réorganisé mon projet, d'un contrôleur contenant une vue de collection à un contrôleur de vue de collection, et il travaillé.

Aller à la figure.

1
répondu Jean Le Moignan 2016-12-09 19:16:34

l'exemple de méthode ci-dessus ne compile pas. Voici une version corrigée (mais non testée quant à savoir si elle fonctionne ou non.)

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes 
{
    let attr: UICollectionViewLayoutAttributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes

    var newFrame = attr.frame
    self.frame = newFrame

    self.setNeedsLayout()
    self.layoutIfNeeded()

    let desiredHeight: CGFloat = self.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    newFrame.size.height = desiredHeight
    attr.frame = newFrame
    return attr
}
0
répondu user3246173 2016-03-14 07:57:26

si vous mettez en œuvre la méthode Uicollection Viewdelegateflowlayout:

- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath*)indexPath

quand vous appelez collectionview performBatchUpdates:completion: , la hauteur de taille utilisera sizeForItemAtIndexPath au lieu de preferredLayoutAttributesFittingAttributes .

le processus de rendu de performBatchUpdates:completion passera par la méthode preferredLayoutAttributesFittingAttributes mais il ignore vos changements.

0
répondu Yang Young 2016-09-19 04:03:06

Update Plus d'information:

  • si vous utilisez flowLayout.estimatedItemSize , suggérez d'utiliser iOS8.3 version ultérieure. Avant iOS8.3, Il s'écrasera [super layoutAttributesForElementsInRect:rect]; . Le message d'erreur est

    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'

  • en Second lieu, dans iOS8.la version x, flowLayout.estimatedItemSize va causer le réglage de section inset différent n'a pas fonctionné. c'est à dire de la fonction: (UIEdgeInsets)collectionView:layout:insetForSectionAtIndex: .

0
répondu Yang Young 2016-10-18 14:54:33

pour tous ceux qui ont tout essayé sans succès, c'est la seule chose qui l'a fait fonctionner pour moi. Pour les étiquettes multilignes à l'intérieur de la cellule, essayez d'ajouter cette ligne magique:

label.preferredMaxLayoutWidth = 200

plus d'informations: ici

santé!

-1
répondu HelloimDarius 2017-05-01 07:51:17

j'ai essayé d'utiliser estimatedItemSize mais il y avait un tas de bogues lors de l'insertion et de la suppression des cellules si le estimatedItemSize n'était pas exactement égale à la hauteur de la cellule. j'ai arrêté de régler estimatedItemSize et j'ai implémenté dynamic cell en utilisant un prototype de cellule. voici comment faire:

créer ce protocole:

protocol SizeableCollectionViewCell {
    func fittedSize(forConstrainedSize size: CGSize)->CGSize
}

mettre en œuvre ce protocole dans votre coutume UICollectionViewCell :

class YourCustomCollectionViewCell: UICollectionViewCell, SizeableCollectionViewCell {

    @IBOutlet private var mTitle: UILabel!
    @IBOutlet private var mDescription: UILabel!
    @IBOutlet private var mContentView: UIView!
    @IBOutlet private var mTitleTopConstraint: NSLayoutConstraint!
    @IBOutlet private var mDesciptionBottomConstraint: NSLayoutConstraint!

    func fittedSize(forConstrainedSize size: CGSize)->CGSize {

        let fittedSize: CGSize!

        //if height is greatest value, then it's dynamic, so it must be calculated
        if size.height == CGFLoat.greatestFiniteMagnitude {

            var height: CGFloat = 0

            /*now here's where you want to add all the heights up of your views.
              apple provides a method called sizeThatFits(size:), but it's not 
              implemented by default; except for some concrete subclasses such 
              as UILabel, UIButton, etc. search to see if the classes you use implement 
              it. here's how it would be used:
            */
            height += mTitle.sizeThatFits(size).height
            height += mDescription.sizeThatFits(size).height
            height += mCustomView.sizeThatFits(size).height    //you'll have to implement this in your custom view

            //anything that takes up height in the cell has to be included, including top/bottom margin constraints
            height += mTitleTopConstraint.constant
            height += mDescriptionBottomConstraint.constant

            fittedSize = CGSize(width: size.width, height: height)
        }
        //else width is greatest value, if not, you did something wrong
        else {
            //do the same thing that's done for height but with width, remember to include leading/trailing margins in calculations
        }

        return fittedSize
    }
}

faites votre contrôleur de se conformer à UICollectionViewDelegateFlowLayout , et, en ce domaine:

class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout {
    private var mCustomCellPrototype = UINib(nibName: <name of the nib file for your custom collectionviewcell>, bundle: nil).instantiate(withOwner: nil, options: nil).first as! SizeableCollectionViewCell
}

il sera utilisé comme une cellule prototype pour lier des données à et ensuite déterminer comment ces données ont affecté la dimension que vous voulez être dynamique

enfin, le UICollectionViewDelegateFlowLayout's collectionView(:layout:sizeForItemAt:) doit être mis en œuvre:

class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {

    private var mDataSource: [CustomModel]

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

        //bind the prototype cell with the data that corresponds to this index path
        mCustomCellPrototype.bind(model: mDataSource[indexPath.row])    //this is the same method you would use to reconfigure the cells that you dequeue in collectionView(:cellForItemAt:). i'm calling it bind

        //define the dimension you want constrained
        let width = UIScreen.main.bounds.size.width - 20    //the width you want your cells to be
        let height = CGFloat.greatestFiniteMagnitude    //height has the greatest finite magnitude, so in this code, that means it will be dynamic
        let constrainedSize = CGSize(width: width, height: height)

        //determine the size the cell will be given this data and return it
        return mCustomCellPrototype.fittedSize(forConstrainedSize: constrainedSize)
    }
}

et c'est tout. Retourner la taille de la cellule dans collectionView(:layout:sizeForItemAt:) de cette façon m'empêchant d'avoir à utiliser estimatedItemSize , et l'insertion et la suppression de cellules fonctionne parfaitement.

-2
répondu shoe 2017-07-27 21:35:58