web-dev-qa-db-fra.com

Cellules UICollectionView à redimensionnement automatique avec mise en forme automatique

J'essaie d'obtenir l'auto-dimensionnement UICollectionViewCells en utilisant la mise en page automatique, mais je n'arrive pas à faire en sorte que les cellules se rediment elles-mêmes au contenu. Je ne parviens pas à comprendre comment la taille de la cellule est mise à jour à partir du contenu de son contenu dans le contenu de la cellule.

Voici la configuration que j'ai essayée:

  • Personnalisé UICollectionViewCell avec un UITextView dans son contentView.
  • Le défilement pour la UITextView est désactivé.
  • La contrainte horizontale de contentView est: "H: | [_textView (320)]", c'est-à-dire que la UITextView est épinglée à gauche de la cellule avec une largeur explicite de 320.
  • La contrainte verticale de contentView est: "V: | -0 - [_ textView]", c’est-à-dire le UITextView épinglé en haut de la cellule.
  • La contrainte de hauteur de UITextView est définie sur une constante à laquelle les rapports UITextView s'ajustent au texte.

Voici à quoi cela ressemble avec l'arrière-plan de la cellule défini sur rouge et l'arrière-plan UITextView sur bleu: cell background red, UITextView background blue

Je mets le projet que j'ai joué avec GitHub ici .

182
rawbee

Mis à jour pour Swift 4

systemLayoutSizeFittingSize renommé en systemLayoutSizeFitting


Mis à jour pour iOS 9

Après avoir vu ma solution GitHub casser sous iOS 9, j'ai enfin eu le temps d'enquêter sur le problème. J'ai maintenant mis à jour le référentiel afin d'inclure plusieurs exemples de configurations différentes pour l'auto-dimensionnement des cellules. Ma conclusion est que les cellules auto-dimensionnantes sont géniales en théorie mais en désordre en pratique. Un mot de prudence lorsque vous procédez à l'auto-dimensionnement des cellules.

TL; DR

Découvrez mon projet GitHub


Les cellules auto-dimensionnées sont uniquement prises en charge avec la disposition de flux, assurez-vous que c'est bien ce que vous utilisez.

Vous devez configurer deux éléments pour que les cellules de dimensionnement automatique fonctionnent.

1. Définissez estimatedItemSize sur UICollectionViewFlowLayout

La structure de flux deviendra de nature dynamique 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 dimensionnement sur votre sous-classe de cellules

Cela vient en 2 saveurs; Mise en page automatique ou remplacement personnalisé de preferredLayoutAttributesFittingAttributes.

Créer et configurer des cellules avec la mise en page automatique

Je n'entrerai pas dans les détails car il y a un brillant SO post à propos de la configuration des contraintes pour une cellule. Méfiez-vous simplement que Xcode 6 a cassé un tas de choses avec iOS 7, donc, si vous prenez en charge iOS 7, vous devrez faire des choses telles que vous assurer que le paramètre autoresizingMask est défini sur le contenu de la cellule et que ses limites sont définies. est défini comme les limites de la cellule lorsque la cellule est chargée (c'est-à-dire awakeFromNib).

Vous devez savoir que votre cellule doit être plus sérieusement contrainte qu'une cellule en mode tableau. Par exemple, si vous souhaitez que votre largeur soit dynamique, votre cellule a besoin d'une contrainte de hauteur. De même, si vous souhaitez que la hauteur soit dynamique, vous aurez besoin d'une contrainte de largeur pour votre cellule.

Implémentez preferredLayoutAttributesFittingAttributes dans votre cellule personnalisée

Lorsque cette fonction est appelée, votre vue a déjà été configurée avec un contenu (c'est-à-dire cellForItem a été appelé). En supposant que vos contraintes soient 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, ce qui pourrait provoquer des plantages sur votre implémentation si vous ne faites pas attention (Voir plus ici ). Lorsque vous implémentez preferredLayoutAttributesFittingAttributes, vous devez vous assurer de ne modifier qu'une seule fois le cadre de vos attributs de présentation. Si vous ne le faites pas, la mise en page appellera votre mise en œuvre indéfiniment et finira par planter. Une solution consiste à mettre en cache la taille calculée dans votre cellule et à l'invalider chaque fois que vous réutilisez la cellule ou modifiez son contenu, comme je l'ai fait avec la propriété isHeightCalculated.

Expérimentez votre mise en page

À ce stade, vous devriez avoir des cellules dynamiques 'fonctionnant' dans votre collectionView. Je n'ai pas encore trouvé la solution prête à l'emploi suffisante lors de mes tests, alors n'hésitez pas à commenter si vous en avez. On a toujours l’impression que UITableView gagne la bataille du dimensionnement dynamique à mon humble avis.

Mises en garde

Soyez très conscient que si vous utilisez des cellules prototypes pour calculer l’estimation de la taille estimée - ceci se brisera si votre XIB utilise des classes de taille . La raison en est que lorsque vous chargez votre cellule à partir d'un fichier XIB, sa classe de taille est configurée avec Undefined. Cela ne sera interrompu que sur iOS 8 et plus, car sur iOS 7, la classe de taille sera chargée en fonction du périphérique (iPad = Normal-Any, iPhone = Compact-Any). Vous pouvez soit définir la valeur EstimatedItemSize sans charger le fichier XIB, soit charger la cellule à partir du fichier XIB, l'ajouter à la collectionView (pour définir le traitCollection), effectuer la mise en page, puis la supprimer de la vue d'ensemble. Sinon, vous pouvez également demander à votre cellule de remplacer le getter traitCollection et de renvoyer les traits appropriés. C'est à vous.

Dites-moi si j'ai oublié quelque chose, j'espère que j'ai aidé et bonne chance pour le codage


271
Daniel Galasko

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

À l'étape 1, lorsque vous utilisez la disposition automatique, ajoutez simplement un UIView parent unique à la cellule. TOUT dans la cellule doit être une sous-vue du parent. Cela a répondu à tous mes problèmes. Bien que Xcode ajoute cela automatiquement pour UITableViewCells, ce n'est pas le cas (mais cela devrait être le cas) pour UICollectionViewCells. Selon le docs :

Pour configurer l’apparence de votre cellule, ajoutez les vues nécessaires à la présentation du contenu de l’élément de données sous forme de sous-vues à la vue de la propriété contentView. Ne pas ajouter directement des sous-vues à la cellule elle-même.

Ensuite, ignorez entièrement l'étape 3. Ce n'est pas nécessaire.

43
Matt Koala

Dans iOS10, il existe une nouvelle constante appelée UICollectionViewFlowLayout.automaticSize (anciennement UICollectionViewFlowLayoutAutomaticSize), donc à la place:

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

vous pouvez utiliser ceci:

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

Ses performances sont meilleures, en particulier lorsque les cellules de votre vue Collection ont une largeur constante.

Accès à la disposition du flux:

override func viewDidLoad() {
   super.viewDidLoad()

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

Dans iOS 10+, il s'agit d'un processus très simple en 2 étapes.

  1. Assurez-vous que tout le contenu de votre cellule est placé dans une seule UIView (ou dans un descendant de UIView comme UIStackView, ce qui simplifie beaucoup la mise en mémoire automatique). Tout comme avec le redimensionnement dynamique de UITableViewCells, toute la hiérarchie de vues doit avoir des contraintes configurées, du conteneur le plus externe à la vue la plus à l'intérieur. Cela inclut les contraintes entre UICollectionViewCell et la vue enfant immédiate

  2. Indiquez à Flowlayout de votre UICollectionView de le redimensionner automatiquement

    yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    
29
Marmoy
  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. Définissez également un UIView comme conteneur principal pour votre cellule et ajoutez toutes les vues requises à l'intérieur de celle-ci.

  3. Reportez-vous à ce didacticiel génial et époustouflant pour une référence ultérieure: ICollectionView avec cellule à taille automatique à l'aide de la mise en pause automatique sous iOS 9 & 1

12
karenms

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
6
noobular

J'ai créé une hauteur de cellule dynamique dans la vue de collection. Voici repo hub git .

Et, expliquez pourquoi preferredLayoutAttributesFittingAttributes est appelée plus d'une fois. En fait, il sera appelé au moins 3 fois.

L'image du journal de la console: enter image description here

1st preferredLayoutAttributesFittingAttributes :

(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.frame.size.height est le statut actuel 57.5 .

2nd preferredLayoutAttributesFittingAttributes :

(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 du cadre de la cellule a été modifiée en 534.5 comme prévu. Mais, la vue de la collection reste à zéro.

3rd 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 que la hauteur de la vue de collection est passée de 0 à 477 .

Le comportement est similaire à gérer le défilement:

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. J'ai donc 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;

provoquera une boucle infinie de l'appel système et un blocage de l'application.

Toute taille modifiée, elle validera encore et encore les couches preferredLayoutAttributesFittingAttributes de toutes les cellules jusqu'à ce que les positions de chaque cellule (c'est-à-dire les cadres) ne soient plus modifiées.

2
Yang Young

Si vous implémentez la méthode UICollectionViewDelegateFlowLayout:

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

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

Le processus de rendu de performBatchUpdates:completion suivra la méthode preferredLayoutAttributesFittingAttributes mais il ignorera vos modifications.

1
Yang Young

À qui que ce soit, cela peut aider,

J'ai eu ce crash si estimatedItemSize était réglé. Même si je suis retourné 0 dans numberOfItemsInSection. Par conséquent, les cellules elles-mêmes et leur mise en page automatique n'étaient pas la cause du crash ... La collectionView venait de tomber en panne, même lorsqu'elle était vide, simplement parce que estimatedItemSize était défini pour un auto-dimensionnement.

Dans mon cas, j'ai réorganisé mon projet, d'un contrôleur contenant une collectionView à une collectionViewController, et cela a fonctionné.

Allez comprendre.

1
Jean Le Moignan

L'exemple de méthode ci-dessus ne compile pas. Voici une version corrigée (mais non testée pour savoir si cela 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
user3246173

Mettre à jour plus d'informations:

  • Si vous utilisez flowLayout.estimatedItemSize, suggérez d'utiliser iOS8.3 version ultérieure. Avant iOS8.3, il se planterait [super layoutAttributesForElementsInRect:rect];. Le message d'erreur est

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

  • Deuxièmement, dans la version iOS8.x, flowLayout.estimatedItemSize entraînera un réglage différent de l'insert de section. c'est-à-dire fonction: (UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:.

0
Yang Young

En plus des réponses ci-dessus,

Assurez-vous simplement que vous définissez installedItemSize propriété de UICollectionViewFlowLayout à une taille et à - ne pas implémenter sizeForItem: atIndexPath méthode déléguée.

C'est ça.

0
Murat Yasar