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?
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.
À 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 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
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
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;
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.
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!
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()
}
}
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
}
}
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.
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
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
}
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
}
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()
}