web-dev-qa-db-fra.com

UICollectionView - hauteur de cellule dynamique?

J'ai besoin d'afficher un tas de collectionViewCells qui ont des hauteurs différentes. les vues sont trop complexes et je ne veux pas calculer manuellement la hauteur attendue. Je veux imposer la mise en page automatique pour calculer la hauteur de la cellule

L'appel de dequeueReusableCellWithReuseIdentifier en dehors de cellForItemAtIndexPath interrompt collectionView et provoque son blocage.

Un autre problème est que la cellule ne se trouve pas dans une xib séparée. Je ne peux donc pas instancier manuellement une cellule temporaire et l'utiliser pour le calcul de la hauteur.

Des solutions pour cela?

public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {

    var cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as UICollectionViewCell
    configureCell(cell, item: items[indexPath.row])

    cell.contentView.setNeedsLayout()
    cell.contentView.layoutIfNeeded()

    return cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
}

MODIFIER:

Un crash se produit dès que dequeueReusableCellWithReuseIdentifier est appelé. Si je n'appelle pas cette méthode et ne retourne qu'une taille, tout fonctionne correctement et les cellules s'affichent sans la taille calculée.

les tailles négatives ou nulles ne sont pas prises en charge dans la présentation du flux

2015-01-26 18:24:34.231 [13383:9752256] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0]'
*** First throw call stack:
(
    0   CoreFoundation                      0x00000001095aef35 __exceptionPreprocess + 165
    1   libobjc.A.dylib                     0x0000000109243bb7 objc_exception_throw + 45
    2   CoreFoundation                      0x0000000109499f33 -[__NSArrayM objectAtIndex:] + 227
    3   UIKit                               0x0000000107419d9c -[UICollectionViewFlowLayout _getSizingInfos] + 842
    4   UIKit                               0x000000010741aca9 -[UICollectionViewFlowLayout _fetchItemsInfoForRect:] + 526
    5   UIKit                               0x000000010741651f -[UICollectionViewFlowLayout prepareLayout] + 257
    6   UIKit                               0x000000010742da10 -[UICollectionViewData _prepareToLoadData] + 67
    7   UIKit                               0x00000001074301c6 -[UICollectionViewData layoutAttributesForItemAtIndexPath:] + 44
    8   UIKit                               0x00000001073fddb1 -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:] + 248
    9                                       0x00000001042b824c _TFC1228BasePaginatingViewController14collectionViewfS0_FTCSo16UICollectionView6layoutCSo22UICollectionViewLayout22sizeForItemAtIndexPathCSo11NSIndexPath_VSC6CGSize + 700
    10                                     0x00000001042b83d4 _TToFC1228BasePaginatingViewController14collectionViewfS0_FTCSo16UICollectionView6layoutCSo22UICollectionViewLayout22sizeForItemAtIndexPathCSo11NSIndexPath_VSC6CGSize + 100
    11  UIKit                               0x0000000107419e2e -[UICollectionViewFlowLayout _getSizingInfos] + 988
    12  UIKit                               0x000000010741aca9 -[UICollectionViewFlowLayout _fetchItemsInfoForRect:] + 526
    13  UIKit                               0x000000010741651f -[UICollectionViewFlowLayout prepareLayout] + 257
    14  UIKit                               0x000000010742da10 -[UICollectionViewData _prepareToLoadData] + 67
    15  UIKit                               0x000000010742e0e9 -[UICollectionViewData validateLayoutInRect:] + 54
    16  UIKit                               0x00000001073f67b8 -[UICollectionView layoutSubviews] + 170
    17  UIKit                               0x0000000106e3c973 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 521
    18  QuartzCore                          0x0000000106b0fde8 -[CALayer layoutSublayers] + 150
    19  QuartzCore                          0x0000000106b04a0e _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 380
    20  QuartzCore                          0x0000000106b0487e _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 24
    21  QuartzCore                          0x0000000106a7263e _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 242
    22  QuartzCore                          0x0000000106a7374a _ZN2CA11Transaction6commitEv + 390
    23  QuartzCore                          0x0000000106a73db5 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 89
    24  CoreFoundation                      0x00000001094e3dc7 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
    25  CoreFoundation                      0x00000001094e3d20 __CFRunLoopDoObservers + 368
    26  CoreFoundation                      0x00000001094d9b53 __CFRunLoopRun + 1123
    27  CoreFoundation                      0x00000001094d9486 CFRunLoopRunSpecific + 470
    28  GraphicsServices                    0x000000010be869f0 GSEventRunModal + 161
    29  UIKit                               0x0000000106dc3420 UIApplicationMain + 1282
    30                                      0x000000010435c709 main + 169
    31  libdyld.dylib                       0x000000010a0f2145 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
43
aryaxt

Voici un tutoriel de Ray Wenderlich qui vous montre comment utiliser AutoLayout pour dimensionner dynamiquement UITableViewCells. Je pense que ce serait la même chose pour UICollectionViewCell

Fondamentalement, vous finissez par mettre en file d'attente, configurer une cellule prototype et saisir sa hauteur. Après avoir lu cet article, j’ai décidé de NOT implémenter cette méthode et d’écrire un code de dimensionnement clair et explicite.

Voici ce que je considère comme la "sauce secrète" de l'article complet:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [self heightForBasicCellAtIndexPath:indexPath];
}

- (CGFloat)heightForBasicCellAtIndexPath:(NSIndexPath *)indexPath {
    static RWBasicCell *sizingCell = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWBasicCellIdentifier];
    });

    [self configureBasicCell:sizingCell atIndexPath:indexPath];
    return [self calculateHeightForConfiguredSizingCell:sizingCell];
}

- (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell {
    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];

    CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    return size.height + 1.0f; // Add 1.0f for the cell separator height
}


EDIT: J’ai fait quelques recherches sur votre crash et décidé qu’il n’y avait aucun moyen de le faire sans un fichier XIB personnalisé. Bien que cela soit un peu frustrant, vous devriez être capable de couper et coller de votre Storyboard vers un fichier XIB personnalisé et vide.

Une fois que vous avez terminé, utilisez le code suivant:

//  ViewController.m
#import "ViewController.h"
#import "CollectionViewCell.h"
@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout> {

}
@property (weak, nonatomic) IBOutlet CollectionViewCell *cell;
@property (weak, nonatomic) IBOutlet UICollectionView   *collectionView;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor lightGrayColor];
    [self.collectionView registerNib:[UINib nibWithNibName:@"CollectionViewCell" bundle:nil] forCellWithReuseIdentifier:@"cell"];
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear...");
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return 50;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
    return 10.0f;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
    return 10.0f;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    return [self sizingForRowAtIndexPath:indexPath];
}
- (CGSize)sizingForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *title                  = @"This is a long title that will cause some wrapping to occur. This is a long title that will cause some wrapping to occur.";
    static NSString *subtitle               = @"This is a long subtitle that will cause some wrapping to occur. This is a long subtitle that will cause some wrapping to occur.";
    static NSString *buttonTitle            = @"This is a really long button title that will cause some wrapping to occur.";
    static CollectionViewCell *sizingCell   = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sizingCell                          = [[NSBundle mainBundle] loadNibNamed:@"CollectionViewCell" owner:self options:nil][0];
    });
    [sizingCell configureWithTitle:title subtitle:[NSString stringWithFormat:@"%@: Number %d.", subtitle, (int)indexPath.row] buttonTitle:buttonTitle];
    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];
    CGSize cellSize = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    NSLog(@"cellSize: %@", NSStringFromCGSize(cellSize));
    return cellSize;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *title                  = @"This is a long title that will cause some wrapping to occur. This is a long title that will cause some wrapping to occur.";
    static NSString *subtitle               = @"This is a long subtitle that will cause some wrapping to occur. This is a long subtitle that will cause some wrapping to occur.";
    static NSString *buttonTitle            = @"This is a really long button title that will cause some wrapping to occur.";
    CollectionViewCell *cell                = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath];
    [cell configureWithTitle:title subtitle:[NSString stringWithFormat:@"%@: Number %d.", subtitle, (int)indexPath.row] buttonTitle:buttonTitle];
    return cell;
}
@end

Le code ci-dessus (avec une sous-classe UICollectionViewCell très basique et XIB associé) me donne ceci:

enter image description here

22
mbm29414

Je viens de rencontrer ce problème sur UICollectionView et sur la façon dont je l'ai résolu, semblable à la réponse ci-dessus, mais d'une manière purement UICollectionView.

  1. Créez une UICollectionViewCell personnalisée contenant tout ce que vous allez remplir pour la rendre dynamique. J'ai créé son propre fichier .xib car cela semble être l'approche la plus simple.

  2. Ajoutez des contraintes dans ce fichier .xib permettant à la cellule d'être calculée de haut en bas. Le redimensionnement ne fonctionnera pas si vous n'avez pas pris en compte toute la hauteur. Supposons que vous ayez une vue en haut, puis une étiquette en dessous et une autre en dessous. Vous devez connecter les contraintes en haut de la cellule en haut de la vue, puis en bas de la vue en haut de la première étiquette, en bas de la première étiquette en haut de la deuxième étiquette et en bas de la deuxième étiquette. au bas de la cellule.

  3. Chargez le fichier .xib dans le contrôleur de vue et enregistrez-le auprès de collectionView on viewDidLoad

    let nib = UINib(nibName: CustomCellName, bundle: nil)
    self.collectionView!.registerNib(nib, forCellWithReuseIdentifier: "customCellID")`
    
  4. Chargez une deuxième copie de cette xib dans la classe et stockez-la en tant que propriété afin de pouvoir l'utiliser pour déterminer la taille de ce que cette cellule devrait être.

    let sizingNibNew = NSBundle.mainBundle().loadNibNamed(CustomCellName, owner: CustomCellName.self, options: nil) as NSArray
    self.sizingNibNew = (sizingNibNew.objectAtIndex(0) as? CustomViewCell)!
    
  5. Implémentez la UICollectionViewFlowLayoutDelegate dans votre contrôleur de vue. La méthode qui compte s'appelle sizeForItemAtIndexPath. Dans cette méthode, vous devez extraire les données de la source de données associée à cette cellule à partir de chemin d'indexation. Configurez ensuite sizingCell et appelez preferredLayoutSizeFittingSize. La méthode retourne un CGSize composé de la largeur moins les insertions de contenu et de la hauteur renvoyée par self.sizingCell.preferredLayoutSizeFittingSize(targetSize).

    override func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
        guard let data = datasourceArray?[indexPath.item] else {
            return CGSizeZero
        }
        let sectionInset = self.collectionView?.collectionViewLayout.sectionInset
        let widthToSubtract = sectionInset!.left + sectionInset!.right
    
        let requiredWidth = collectionView.bounds.size.width
    
    
        let targetSize = CGSize(width: requiredWidth, height: 0)
    
        sizingNibNew.configureCell(data as! CustomCellData, delegate: self)
        let adequateSize = self.sizingNibNew.preferredLayoutSizeFittingSize(targetSize)
        return CGSize(width: (self.collectionView?.bounds.width)! - widthToSubtract, height: adequateSize.height)
     }
    
  6. Dans la classe de la cellule personnalisée elle-même, vous devrez remplacer awakeFromNib et indiquer à contentView que sa taille doit être flexible.

     override func awakeFromNib() {
        super.awakeFromNib()
        self.contentView.autoresizingMask = [UIViewAutoresizing.FlexibleHeight]
     }
    
  7. Dans la cellule personnalisée, remplacez layoutSubviews

     override func layoutSubviews() {
       self.layoutIfNeeded()
      }
    
  8. Dans la classe de la cellule personnalisée, implémentez preferredLayoutSizeFittingSize. C’est là que vous devrez faire des tromperies sur les éléments qui sont disposés. Si c'est une étiquette, vous devrez lui indiquer quelle devrait être sa valeur preferredMaxWidth.

    preferredLayoutSizeFittingSize(targetSize: CGSize)-> CGSize {
    
        let originalFrame = self.frame
        let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth
    
    
        var frame = self.frame
        frame.size = targetSize
        self.frame = frame
    
        self.setNeedsLayout()
        self.layoutIfNeeded()
        self.label.preferredMaxLayoutWidth = self.questionLabel.bounds.size.width
    
    
        // calling this tells the cell to figure out a size for it based on the current items set
        let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
    
        let newSize = CGSize(width:targetSize.width, height:computedSize.height)
    
        self.frame = originalFrame
        self.questionLabel.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth
    
        return newSize
    }
    

Toutes ces étapes devraient vous donner les bonnes tailles. Si vous obtenez 0 ou d'autres numéros géniaux que vous n'avez pas configuré vos contraintes correctement.

24
bolnad

Nous pouvons conserver une hauteur dynamique pour la cellule de vue de collection sans xib (uniquement à l'aide d'un storyboard).

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

        NSAttributedString* labelString = [[NSAttributedString alloc] initWithString:@"Your long string goes here" attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:17.0]}];
        CGRect cellRect = [labelString boundingRectWithSize:CGSizeMake(cellWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin context:nil];

        return CGSizeMake(cellWidth, cellRect.size.height);
}

Assurez-vous que numberOfLines dans IB doit être 0.

13
iNasir

TL; DR: Numérisez une image, puis consultez projet en cours ici.

Mise à jour de ma réponse pour une solution plus simple que j'ai trouvée.

Dans mon cas, je voulais fixer la largeur et avoir des cellules de hauteur variable. Je voulais une solution réactive réutilisable, qui gérait la rotation et ne nécessitait pas beaucoup d'intervention.

Ce à quoi je suis arrivé était que je remplaçais (seulement) systemLayoutFitting(...) dans la cellule de la collection (dans ce cas, une classe de base pour moi) et que vaincs tout d'abord l'effort d'UICollectionView pour définir la mauvaise dimension sur contentView en ajoutant une contrainte pour la dimension connue , la largeur.

class EstimatedWidthCell: UICollectionViewCell {
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.translatesAutoresizingMaskIntoConstraints = false
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        contentView.translatesAutoresizingMaskIntoConstraints = false
    }

    override func systemLayoutSizeFitting(
        _ targetSize: CGSize, withHorizontalFittingPriority
        horizontalFittingPriority: UILayoutPriority,
        verticalFittingPriority: UILayoutPriority) -> CGSize {

        width.constant = targetSize.width

puis retourne la taille finale de la cellule - utilisée pour (et cela ressemble à un bogue) la dimension de la cellule elle-même, mais pas contentView - qui est par ailleurs contrainte à une taille en conflit (d'où la contrainte ci-dessus). Pour calculer la taille de cellule correcte, j'utilise une priorité inférieure pour la dimension que je voulais flotter et je récupère la hauteur requise pour que le contenu s'adapte à la largeur que je veux corriger:

        let size = contentView.systemLayoutSizeFitting(
            CGSize(width: targetSize.width, height: 1),
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: verticalFittingPriority)

        print("\(#function) \(#line) \(targetSize) -> \(size)")
        return size
    }

    lazy var width: NSLayoutConstraint = {
        return contentView.widthAnchor
            .constraint(equalToConstant: bounds.size.width)
            .isActive(true)
    }()
}

Mais d'où vient cette largeur? Il est configuré via la variable estimatedItemSize dans la présentation du flux de la vue de collection:

lazy var collectionView: UICollectionView = {
    let view = UICollectionView(frame: CGRect(), collectionViewLayout: layout)
    view.backgroundColor = .cyan
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
}()

lazy var layout: UICollectionViewFlowLayout = {
    let layout = UICollectionViewFlowLayout()
    let width = view.bounds.size.width // should adjust for inset
    layout.estimatedItemSize = CGSize(width: width, height: 10)
    layout.scrollDirection = .vertical
    return layout
}()

Enfin, pour gérer la rotation, j'implémente trailCollectionDidChange pour invalider la mise en page:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    layout.estimatedItemSize = CGSize(width: view.bounds.size.width, height: 10)
    layout.invalidateLayout()
    super.traitCollectionDidChange(previousTraitCollection)
}

Le résultat final ressemble à ceci:

enter image description here

Et j'ai publié un échantillon de travail ici.

8
Chris Conover

Swift 4. *

J'ai créé un fichier Xib pour UICollectionViewCell, ce qui semble être la bonne approche.

extension ViewController: UICollectionViewDelegateFlowLayout {

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

    private func size(for indexPath: IndexPath) -> CGSize {
        // load cell from Xib
        let cell = Bundle.main.loadNibNamed("ACollectionViewCell", owner: self, options: nil)?.first as! ACollectionViewCell

        // configure cell with data in it
        let data = self.data[indexPath.item]
        cell.configure(withData: data)

        cell.setNeedsLayout()
        cell.layoutIfNeeded()

        // width that you want
        let width = collectionView.frame.width
        let height: CGFloat = 0

        let targetSize = CGSize(width: width, height: height)

        // get size with width that you want and automatic height
        let size = cell.contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .fittingSizeLevel)
        // if you want height and width both to be dynamic use below
        // let size = cell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)

        return size
    }
}

Note: Je ne recommande pas de définir l'image lors de la configuration de données dans ce cas déterminant la taille. Cela m'a donné le résultat déformé/indésirable. La configuration des textes ne m'a donné que le résultat ci-dessous.

 enter image description here

3
Dari

On dirait que c'est une question très populaire, alors je vais essayer de faire mon humble contribution.


Le code ci-dessous est Swift 4 solution pour une configuration sans scénarimage. Il utilise certaines approches des réponses précédentes, par conséquent, il empêche l'avertissement de mise en forme automatique causé par la rotation du périphérique. 

Je suis désolé si les exemples de code sont un peu longs. Je souhaite fournir une solution "facile à utiliser" entièrement hébergée par StackOverflow. Si vous avez des suggestions à faire à la poste, partagez l’idée et je la mettrai à jour en conséquence. 

La mise en place:

Deux classes: ViewController.Swift et MultilineLabelCell.Swift - Cellule contenant une seule variable UILabel

MultilineLabelCell.Swift

import UIKit

class MultilineLabelCell: UICollectionViewCell {
    static let reuseId = "MultilineLabelCellReuseId"

    private let label: UILabel = UILabel(frame: .zero)

    override init(frame: CGRect) {
        super.init(frame: frame)

        layer.borderColor = UIColor.red.cgColor
        layer.borderWidth = 1.0

        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping

        let labelInset = UIEdgeInsets(top: 10, left: 10, bottom: -10, right: -10)
        contentView.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor, constant: labelInset.top).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor, constant: labelInset.left).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor, constant: labelInset.right).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: labelInset.bottom).isActive = true

        label.layer.borderColor = UIColor.black.cgColor
        label.layer.borderWidth = 1.0
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("Storyboards are quicker, easier, more seductive. Not stronger then Code.")
    }

    func configure(text: String?) {
        label.text = text
    }

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

ViewController.Swift

import UIKit

let samuelQuotes = [
    "Samuel says", 
    "Add different length strings here for better testing"
]

class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    private(set) var collectionView: UICollectionView

    // Initializers
    init() {
        // Create new `UICollectionView` and set `UICollectionViewFlowLayout` as its layout
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        // Create new `UICollectionView` and set `UICollectionViewFlowLayout` as its layout
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
        super.init(coder: aDecoder)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Dynamic size sample"

        // Register Cells
        collectionView.register(MultilineLabelCell.self, forCellWithReuseIdentifier: MultilineLabelCell.reuseId)

        // Add `coolectionView` to display hierarchy and setup its appearance
        view.addSubview(collectionView)
        collectionView.backgroundColor = .white
        collectionView.contentInsetAdjustmentBehavior = .always
        collectionView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

        // Setup Autolayout constraints
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
        collectionView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
        collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
        collectionView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true

        // Setup `dataSource` and `delegate`
        collectionView.dataSource = self
        collectionView.delegate = self

        (collectionView.collectionViewLayout as! UICollectionViewFlowLayout).estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        (collectionView.collectionViewLayout as! UICollectionViewFlowLayout).sectionInsetReference = .fromLayoutMargins
    }

    // MARK: - UICollectionViewDataSource -
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MultilineLabelCell.reuseId, for: indexPath) as! MultilineLabelCell
        cell.configure(text: samuelQuotes[indexPath.row])
        return cell
    }

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

    // MARK: - UICollectionViewDelegateFlowLayout -
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let sectionInset = (collectionViewLayout as! UICollectionViewFlowLayout).sectionInset
        let referenceHeight: CGFloat = 100 // Approximate height of your cell
        let referenceWidth = collectionView.safeAreaLayoutGuide.layoutFrame.width
            - sectionInset.left
            - sectionInset.right
            - collectionView.contentInset.left
            - collectionView.contentInset.right
        return CGSize(width: referenceWidth, height: referenceHeight)
    }
}

Pour exécuter cet exemple de création d'un nouveau projet Xcode, créez les fichiers correspondants et remplacez le contenu de AppDelegate par le code suivant:

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var navigationController: UINavigationController?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)

        if let window = window {
            let vc = ViewController()
            navigationController = UINavigationController(rootViewController: vc)
            window.rootViewController = navigationController
            window.makeKeyAndVisible()
        }

        return true
    }
}
2
fewlinesofcode

J'ai suivi les étapes mentionnées dans ce SO et tout va bien, sauf lorsque ma Collection View contient moins de données (texte) pour les rendre suffisamment larges. En vérifiant la documentation dans systemLyaoutSizeFittingSize, j'ai cette solution afin que ma cellule prenne la largeur demandée:

- (CGSize)calculateSizeForSizingCell:(UICollectionViewCell *)sizingCell width:(CGFloat)width {
    CGRect frame = sizingCell.frame;
    frame.size.width = width;
    sizingCell.frame = frame;
    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];

    CGSize size = [sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize
                        withHorizontalFittingPriority:UILayoutPriorityRequired
                              verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    return size;
}

J'espère que cela aiderait quelqu'un.

- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize NS_AVAILABLE_IOS(6_0);

Doc Apple:

Équivalent à envoyer -systemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority: avec UILayoutPriorityFittingSizeLevel pour les deux priorités.

Alors que la valeur par défaut est "assez basse" selon la documentation d'Apple:

Lorsque vous envoyez - [UIView systemLayoutSizeFittingSize:], la taille correspondant le mieux à la taille cible (l'argument) est calculée. UILayoutPriorityFittingSizeLevel est le niveau de priorité avec lequel la vue veut se conformer à la taille cible dans ce calcul. C'est assez bas. Il n’est généralement pas approprié de créer une contrainte exactement à cette priorité. Vous voulez être plus haut ou plus bas.

Donc, mon changement de comportement par défaut consiste à appliquer la largeur (ajustement horizontal) avec UILayoutPriorityRequired

1
John Pang

Cela a fonctionné pour moi, espérons que vous aussi.

* Remarque: j'ai utilisé la mise en page automatique dans Nib, n'oubliez pas d'ajouter des contraintes supérieures et inférieures pour les vues secondaires dans contentView

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
           let cell = YourCollectionViewCell.instantiateFromNib()
           cell.frame.size.width = collectionView.frame.width
           cell.data = viewModel.data[indexPath.item]
           let resizing = cell.systemLayoutSizeFitting(UILayoutFittingCompressedSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.fittingSizeLevel)
           return resizing
    }
0
logan.Nguyen

Réponse rapide de Swift 4 sur la base de la réponse utile de @ mbm29414.

Malheureusement, cela nécessite l’utilisation d’un fichier XIB. Il ne semble pas y avoir d'alternative.

Les pièces clés utilisent une cellule de dimensionnement (créée une seule fois) et enregistrent le fichier XIB lors de l'initialisation de la vue de collection.

Ensuite, vous dimensionner chaque cellule dynamiquement dans la fonction sizeForItemAt.

// UICollectionView Vars and Constants
let CellXIBName = YouViewCell.XIBName
let CellReuseID = YouViewCell.ReuseID
var sizingCell = YouViewCell()


fileprivate func initCollectionView() {
    // Connect to view controller
    collectionView.dataSource = self
    collectionView.delegate = self

    // Register XIB
    collectionView.register(UINib(nibName: CellXIBName, bundle: nil), forCellWithReuseIdentifier: CellReuseID)

    // Create sizing cell for dynamically sizing cells
    sizingCell = Bundle.main.loadNibNamed(CellXIBName, owner: self, options: nil)?.first as! YourViewCell

    // Set scroll direction
    let layout = UICollectionViewFlowLayout()
    layout.scrollDirection = .vertical
    collectionView.collectionViewLayout = layout

    // Set properties
    collectionView.alwaysBounceVertical = true
    collectionView.alwaysBounceHorizontal = false

    // Set top/bottom padding
    collectionView.contentInset = UIEdgeInsets(top: collectionViewTopPadding, left: collectionViewSidePadding, bottom: collectionViewBottomPadding, right: collectionViewSidePadding)

    // Hide scrollers
    collectionView.showsVerticalScrollIndicator = false
    collectionView.showsHorizontalScrollIndicator = false
}


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    // Get cell data and render post
    let data = YourData[indexPath.row]
    sizingCell.renderCell(data: data)

    // Get cell size
    sizingCell.setNeedsLayout()
    sizingCell.layoutIfNeeded()
    let cellSize = sizingCell.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)

    // Return cell size
    return cellSize
}
0
Crashalot

Suivez bolnad answer jusqu’à l’étape 4.

Simplifiez ensuite les choses en remplaçant toutes les autres étapes par:

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

    // Configure your cell
    sizingNibNew.configureCell(data as! CustomCellData, delegate: self)

    // We use the full width minus insets
    let width = collectionView.frame.size.width - collectionView.sectionInset.left - collectionView.sectionInset.right

    // Constrain our cell to this width 
    let height = sizingNibNew.systemLayoutSizeFitting(CGSize(width: width, height: .infinity), withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityFittingSizeLevel).height

    return CGSize(width: width, height: height)
}
0
Rivera