web-dev-qa-db-fra.com

UICollectionView dans une UITableViewCell - hauteur dynamique?

Un des écrans de nos applications nous oblige à placer une UICollectionView dans une UITableViewCell. Cette UICollectionView aura un nombre dynamique d’éléments, ce qui donnera une hauteur qui devra également être calculée dynamiquement. Cependant, je rencontre des problèmes pour essayer de calculer la hauteur de la variable UICollectionView incorporée.

Notre UIViewController globale a été créée dans Storyboards et utilise la mise en page automatique. Mais je ne sais pas comment augmenter dynamiquement la hauteur de UITableViewCell en fonction de la hauteur de UICollectionView.

Quelqu'un peut-il donner des conseils ou des astuces sur la manière de procéder?

80
Shadowman

La bonne réponse est OUI, vous POUVEZ le faire.

Je suis tombé sur ce problème il y a quelques semaines. C'est en fait plus facile que vous ne le pensez. Placez vos cellules dans des NIB (ou des story-boards) et épinglez-les pour laisser la mise en page automatique faire tout le travail.

Compte tenu de la structure suivante:

TableView 

TableViewCell

CollectionView

CollectionViewCell

CollectionViewCell

CollectionViewCell

[... nombre variable de cellules ou tailles de cellules différentes] 

La solution consiste à indiquer à la disposition automatique de calculer d'abord les tailles de collectionViewCell, puis la vue de collection contentSize, et de l'utiliser comme taille de votre cellule. C'est la méthode UIView qui "fait la magie":

-(void)systemLayoutSizeFittingSize:(CGSize)targetSize 
     withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority 
           verticalFittingPriority:(UILayoutPriority)verticalFittingPriority

Vous devez définir ici la taille de TableViewCell, qui est dans votre cas le contentSize de CollectionView.

CollectionViewCell

À la CollectionViewCell, vous devez indiquer à la cellule la mise en page chaque fois que vous modifiez le modèle (par exemple: vous définissez un UILabel avec un texte, la cellule doit être à nouveau mise en page).

- (void)bindWithModel:(id)model {
    // Do whatever you may need to bind with your data and 
    // tell the collection view cell's contentView to resize
    [self.contentView setNeedsLayout];
}
// Other stuff here...

TableViewCell

TableViewCell fait la magie. Il a une sortie vers votre collectionView, active la mise en page automatique pour les cellules collectionView à l'aide de installedItemSize de UICollectionViewFlowLayout.

Ensuite, l'astuce consiste à utiliser la méthode pour définir la taille de votre cellule tableView à la méthode systemLayoutSizeFittingSize... (REMARQUE: iOS8 ou version ultérieure)

REMARQUE: j'ai essayé d'utiliser la méthode de hauteur de la cellule déléguée de tableView -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath. Mais il est trop tard pour que le système de présentation automatique calcule le contenu de CollectionView contentSize.

@implementation TableCell

- (void)awakeFromNib {
    [super awakeFromNib];
    UICollectionViewFlowLayout *flow = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;

    // Configure the collectionView
    flow.minimumInteritemSpacing = ...;

    // This enables the magic of auto layout. 
    // Setting estimatedItemSize different to CGSizeZero 
    // on flow Layout enables auto layout for collectionView cells.
    // https://developer.Apple.com/videos/play/wwdc2014-226/
    flow.estimatedItemSize = CGSizeMake(1, 1);

    // Disable the scroll on your collection view
    // to avoid running into multiple scroll issues.
    [self.collectionView setScrollEnabled:NO];
}

- (void)bindWithModel:(id)model {
    // Do your stuff here to configure the tableViewCell
    // Tell the cell to redraw its contentView        
    [self.contentView layoutIfNeeded];
}

// THIS IS THE MOST IMPORTANT METHOD
//
// This method tells the auto layout
// You cannot calculate the collectionView content size in any other place, 
// because you run into race condition issues.
// NOTE: Works for iOS 8 or later
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority {

    // With autolayout enabled on collection view's cells we need to force a collection view relayout with the shown size (width)
    self.collectionView.frame = CGRectMake(0, 0, targetSize.width, MAXFLOAT);
    [self.collectionView layoutIfNeeded];

    // If the cell's size has to be exactly the content 
    // Size of the collection View, just return the
    // collectionViewLayout's collectionViewContentSize.

    return [self.collectionView.collectionViewLayout collectionViewContentSize];
}

// Other stuff here...

@end

TableViewController

N'oubliez pas d'activer le système de présentation automatique pour les cellules tableView sur votre TableViewController:

- (void)viewDidLoad {
    [super viewDidLoad];

    // Enable automatic row auto layout calculations        
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    // Set the estimatedRowHeight to a non-0 value to enable auto layout.
    self.tableView.estimatedRowHeight = 10;

}

CREDIT: @rbarbera a aidé à résoudre ce problème 

91
Pablo Romeu

Je pense, mon chemin est beaucoup plus simple, puis proposé par @PabloRomeu.

Étape 1. Créez un point de vente de UICollectionView à UITableViewCell subclass, où UICollectionView est placé. Laissez, son nom sera collectionView

Étape 2. Ajoutez dans IB pour la contrainte UICollectionView height et créez un point de vente sur UITableViewCell subclass également. Laissez, son nom sera collectionViewHeight.

Étape 3. Dans tableView:cellForRowAtIndexPath:, ajoutez le code:

// deque a cell
cell.frame = tableView.bounds;
[cell layoutIfNeeded];
[cell.collectionView reloadData];
cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height;
61
Igor

Les vues de table et les vues de collection sont des sous-classes UIScrollView et n'apprécient donc pas d'être intégrées à une autre vue de défilement car elles tentent de calculer la taille du contenu, de réutiliser des cellules, etc.

Je vous recommande d'utiliser uniquement une vue de collection pour tous vos besoins.

Vous pouvez le diviser en sections et "traiter" la disposition de certaines sections en tant que vue de tableau et d'autres en tant que vue de collection. Après tout, rien n’est impossible avec une vue de collection que vous pouvez utiliser avec une vue sous forme de tableau.

Si vous disposez d'une disposition de grille de base pour votre vue de collection "pièces", vous pouvez également utiliser des cellules de tableau standard pour les gérer. Néanmoins, si vous n'avez pas besoin de la prise en charge d'iOS 5, vous devriez mieux utiliser les vues de collection.

16
Rivera

La réponse de Pablo Romeu ci-dessus ( https://stackoverflow.com/a/33364092/2704206 ) m'a énormément aidé avec mon problème. Je devais cependant faire certaines choses différemment pour que cela fonctionne pour mon problème. Tout d'abord, je n'ai pas eu à appeler layoutIfNeeded() aussi souvent. Je n'avais qu'à l'appeler sur la variable collectionView dans la fonction systemLayoutSizeFitting

Deuxièmement, j'avais des contraintes de mise en page automatique sur la vue Collection dans la cellule Vue Tableau pour lui donner un peu de remplissage. J'ai donc dû soustraire les marges de début et de fin du targetSize.width lors de la définition de la largeur du collectionView.frame. J'ai également dû ajouter les marges supérieure et inférieure à la valeur de retour CGSize height. 

Pour obtenir ces constantes de contrainte, j'avais la possibilité de créer des points de vente pour les contraintes, de coder en dur leurs constantes ou de les rechercher à l'aide d'un identifiant. J'ai décidé d'utiliser la troisième option pour rendre ma classe de cellules d'affichage de table personnalisée facilement réutilisable. À la fin, c’était tout ce dont j'avais besoin pour que cela fonctionne:

class CollectionTableViewCell: UITableViewCell {

    // MARK: -
    // MARK: Properties
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionViewLayout?.estimatedItemSize = CGSize(width: 1, height: 1)
            selectionStyle = .none
        }
    }

    var collectionViewLayout: UICollectionViewFlowLayout? {
        return collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    }

    // MARK: -
    // MARK: UIView functions
    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()

        let topConstraintConstant = contentView.constraint(byIdentifier: "topAnchor")?.constant ?? 0
        let bottomConstraintConstant = contentView.constraint(byIdentifier: "bottomAnchor")?.constant ?? 0
        let trailingConstraintConstant = contentView.constraint(byIdentifier: "trailingAnchor")?.constant ?? 0
        let leadingConstraintConstant = contentView.constraint(byIdentifier: "leadingAnchor")?.constant ?? 0

        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width - trailingConstraintConstant - leadingConstraintConstant, height: 1)

        let size = collectionView.collectionViewLayout.collectionViewContentSize
        let newSize = CGSize(width: size.width, height: size.height + topConstraintConstant + bottomConstraintConstant)
        return newSize
    }
}

En tant que fonction d'assistance permettant de récupérer une contrainte par identifiant, j'ajoute l'extension suivante:

extension UIView {
    func constraint(byIdentifier identifier: String) -> NSLayoutConstraint? {
        return constraints.first(where: { $0.identifier == identifier })
    }
}

REMARQUE: Vous devrez définir l'identifiant pour ces contraintes dans votre storyboard ou à tout autre endroit où elles sont créées. À moins qu'ils aient une constante 0, alors cela n'a pas d'importance. De même, comme dans la réponse de Pablo, vous devrez utiliser UICollectionViewFlowLayout comme mise en page pour votre vue de collection. Enfin, assurez-vous de lier la collectionView IBOutlet à votre scénarimage.

Avec la cellule de vue de table personnalisée ci-dessus, je peux maintenant la sous-classer dans toute autre cellule nécessitant une vue de collection et la faire implémenter pour mettre en œuvre les protocoles UICollectionViewDelegateFlowLayout et UICollectionViewDataSource. J'espère que cela sera utile à quelqu'un d'autre!

3
jmad8

Une alternative à la solution de Pablo Romeu est de personnaliser UICollectionView lui-même, plutôt que de faire le travail dans la cellule vue tableau. 

Le problème sous-jacent est que, par défaut, une vue de collection n'a pas de taille intrinsèque et ne peut donc pas informer la mise en page automatique des dimensions à utiliser. Vous pouvez y remédier en créant une sous-classe personnalisée qui renvoie une taille intrinsèque utile.

Créez une sous-classe de UICollectionView et substituez les méthodes suivantes

override func intrinsicContentSize() -> CGSize {
    self.layoutIfNeeded()

    var size = super.contentSize
    if size.width == 0 || size.height == 0 {
        // return a default size
        size = CGSize(width: 600, height:44)
    }

    return size
 }

override func reloadData() {
    super.reloadData()
    self.layoutIfNeeded()
    self.invalidateIntrinsicContentSize()
}

(Vous devez également remplacer les méthodes associées: reloadSections, reloadItemsAtIndexPaths de la même manière que reloadData ()).

L'appel de layoutIfNeeded oblige la vue Collection à recalculer la taille du contenu, qui peut ensuite être utilisée comme nouvelle taille intrinsèque.

De plus, vous devez gérer explicitement les modifications apportées à la taille de la vue (par exemple lors de la rotation du périphérique) dans le contrôleur de vue de table.

    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    dispatch_async(dispatch_get_main_queue()) {
        self.tableView.reloadData()
    }
}
3
Dale

Peut-être que ma variante sera utile. J'ai décidé de cette tâche au cours des deux dernières heures. Je ne prétends pas que c'est correct ou optimal à 100%, mais ma compétence est encore très petite et j'aimerais entendre les commentaires des experts. Je vous remercie. Une remarque importante: cela fonctionne pour la table statique - il est spécifié par mon travail actuel . Donc, tout ce que j'utilise est viewWillLayoutSubviews de tableView . Et un peu plus.

private var iconsCellHeight: CGFloat = 500 

func updateTable(table: UITableView, withDuration duration: NSTimeInterval) {
    UIView.animateWithDuration(duration, animations: { () -> Void in
        table.beginUpdates()
        table.endUpdates()
    })
}

override func viewWillLayoutSubviews() {
    if let iconsCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 1)) as? CategoryCardIconsCell {
        let collectionViewContentHeight = iconsCell.iconsCollectionView.contentSize.height
        if collectionViewContentHeight + 17 != iconsCellHeight {
            iconsCellHeight = collectionViewContentHeight + 17
            updateTable(tableView, withDuration: 0.2)
        }
    }
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    switch (indexPath.section, indexPath.row) {
        case ...
        case (1,0):
            return iconsCellHeight
        default:
            return tableView.rowHeight
    }
}
  1. Je sais que la collectionView est située dans la première rangée de la deuxième section;
  2. Laissez la hauteur de la ligne est de 17 p. plus grand que sa hauteur de contenu;
  3. iconsCellHeight est un nombre aléatoire au démarrage du programme (je sais que sous la forme portrait, il doit être exactement 392, mais ce n'est pas important). Si le contenu de collectionView + 17 n'est pas égal à ce nombre, changez donc sa valeur. La prochaine fois, dans cette situation, la condition donne FALSE;
  4. Après tout mettre à jour la tableView. Dans mon cas, il s’agit de la combinaison de deux opérations (pour la mise à jour Nice des lignes en extension);
  5. Et bien sûr, dans le heightForRowAtIndexPath, ajoutez une ligne au code.
2

Je mettrais une méthode statique sur la classe de vue de collection qui renverra une taille basée sur le contenu qu'elle aura. Utilisez ensuite cette méthode dans la variable heightForRowAtIndexPath pour renvoyer la taille appropriée.

Notez également que vous pouvez avoir un comportement étrange lorsque vous intégrez ce type de viewControllers. Je l'ai fait une fois et j'avais des problèmes de mémoire étranges que je n'avais jamais résolus.

2
InkGolem

L’approche la plus simple que j’ai proposée jusqu’à présent, les crédits à la réponse @igor ci-dessus,

Dans votre classe tableviewcell, insérez simplement ceci

override func layoutSubviews() {
    self.collectionViewOutlet.constant = self.postPoll.collectionViewLayout.collectionViewContentSize.height
}

et bien sûr, changez le collectionviewoutlet avec votre sortie dans la classe de la cellule

2
Osa

Je reçois l'idée de @Igor post et j'investis mon temps pour cela dans mon projet avec Swift

Juste passé cela dans votre

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    cell.frame = tableView.bounds
    cell.layoutIfNeeded()
    cell.collectionView.reloadData()
    cell.collectionView.heightAnchor.constraint(equalToConstant: cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
    cell.layoutIfNeeded()
    return cell
}

Ajout: Si vous voyez votre UICollectionView trembler lors du chargement des cellules.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {       
  //do dequeue stuff
  cell.layer.shouldRasterize = true
  cell.layer.rasterizationScale = UIScreen.main.scale
  return cell
}
0
Nazmul Hasan

Je faisais face au même problème récemment et j’ai presque essayé toutes les solutions, certaines fonctionnaient et d’autres ne me tenaient pas principalement à l’approche @ PabloRomeu: si vous avez d’autres contenus dans la cellule (autre que la vue Collection), vous devrez calculer leurs hauteurs et celles de leurs contraintes et renvoyer le résultat pour obtenir une mise en page automatique correcte et je n'aime pas calculer les choses manuellement dans mon code. Voici donc la solution qui a bien fonctionné pour moi sans faire de calcul manuel dans mon code.

dans le cellForRow:atIndexPath de la vue de table, je fais ce qui suit: 

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    //initialize the the collection view data source with the data
    cell.frame = CGRect.zero
    cell.layoutIfNeeded()
    return cell
}

Je pense que ce qui se passe ici est que je force la cellule tableview à ajuster sa hauteur après le calcul de la hauteur de la vue de collection. (après avoir fourni la date collectionView à la source de données)

J'ai lu toutes les réponses. Cela semble servir à tous les cas. 

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()
        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width , height: 1)
        return collectionView.collectionViewLayout.collectionViewContentSize
}
0
SRP-Achiever

La solution de Pablo ne fonctionnait pas très bien pour moi, j'avais des effets visuels étranges (la collectionView ne s'ajuste pas correctement).

Ce qui a bien fonctionné a été d'ajuster la contrainte de hauteur de collectionView (en tant que NSLayoutConstraint) à collectionView contentSize pendant layoutSubviews(). C'est la méthode appelée quand autolayout est appliqué à la cellule.

// Constraint on the collectionView height in the storyboard. Priority set to 999.
@IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!


// Method called by autolayout to layout the subviews (including the collectionView).
// This is triggered with 'layoutIfNeeded()', or by the viewController
// (happens between 'viewWillLayoutSubviews()' and 'viewDidLayoutSubviews()'.
override func layoutSubviews() {

    collectionViewHeightConstraint.constant = collectionView.contentSize.height
    super.layoutSubviews()
}

// Call `layoutIfNeeded()` when you update your UI from the model to trigger 'layoutSubviews()'
private func updateUI() {
    layoutIfNeeded()
}
0
Frédéric Adda