web-dev-qa-db-fra.com

Spécification d'une dimension de cellules dans UICollectionView à l'aide de la disposition automatique

Dans iOS 8, UICollectionViewFlowLayout prend en charge le redimensionnement automatique des cellules en fonction de leur propre taille de contenu. Cela redimensionne les cellules en largeur et en hauteur en fonction de leur contenu.

Est-il possible de spécifier une valeur fixe pour la largeur (ou la hauteur) de toutes les cellules et autoriser le redimensionnement des autres dimensions?

Pour un exemple simple, considérons une étiquette multiligne dans une cellule avec des contraintes la plaçant sur les côtés de la cellule. L'étiquette multiligne pourrait être redimensionnée de différentes manières pour s'adapter au texte. La cellule doit remplir la largeur de la vue de collection et ajuster sa hauteur en conséquence. Au lieu de cela, les cellules sont dimensionnées au hasard et cela provoque même un blocage lorsque la taille de la cellule est supérieure à la dimension sans défilement de la vue Collection.


iOS 8 introduit la méthode systemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority: Pour chaque cellule de la vue de collection, la mise en page appelle cette méthode dans la cellule, en transmettant la taille estimée. Ce qui serait logique pour moi serait de remplacer cette méthode sur la cellule, d'indiquer la taille donnée et de définir la contrainte horizontale sur requis et une priorité faible sur la contrainte verticale. De cette façon, la taille horizontale est fixée à la valeur définie dans la présentation et la taille verticale peut être flexible.

Quelque chose comme ça:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
    attributes.size = [self systemLayoutSizeFittingSize:layoutAttributes.size withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    return attributes;
}

Les tailles rendues par cette méthode, cependant, sont complètement étranges. La documentation sur cette méthode m'est très peu claire et mentionne l'utilisation des constantes UILayoutFittingCompressedSizeUILayoutFittingExpandedSize qui représentent simplement une taille nulle et une assez grande.

Le paramètre size de cette méthode est-il vraiment un moyen de passer deux constantes? N'y a-t-il pas moyen d'obtenir le comportement que j'attendais pour obtenir la hauteur appropriée pour une taille donnée?


Solutions alternatives

1) L'ajout de contraintes spécifiant une largeur spécifique pour la cellule permet une mise en page correcte. Cette solution est médiocre car cette contrainte doit être définie sur la taille de la vue de collection de la cellule à laquelle elle ne fait pas référence. La valeur de cette contrainte peut être transmise lors de la configuration de la cellule, mais cela semble également totalement contre-intuitif. Cela est également gênant, car l’ajout de contraintes directement à une cellule ou à son affichage de contenu pose de nombreux problèmes.

2) Utilisez une vue de table. Les vues sous forme de tableau fonctionnent de cette manière dans la mesure où les cellules ont une largeur fixe, mais cela ne conviendrait pas, par exemple, à une mise en page iPad avec des cellules à largeur fixe dans plusieurs colonnes.

108
Anthony Mattox

Cela ressemble à ce que vous demandez, c’est un moyen d’utiliser UICollectionView pour produire une présentation telle que UITableView. Si c'est vraiment ce que vous voulez, la meilleure façon de le faire est d'utiliser une sous-classe UICollectionViewLayout personnalisée (peut-être quelque chose comme SBTableLayout ).

D'autre part, si vous demandez vraiment s'il existe un moyen propre de le faire avec le paramètre par défaut UICollectionViewFlowLayout, alors je pense qu'il n'y a pas moyen. Même avec les cellules auto-dimensionnées de iOS8, ce n'est pas simple. Le problème fondamental, comme vous le dites, est que les machines de la structure de flux ne fournissent aucun moyen de réparer une dimension et de laisser une autre répondre. (De plus, même si vous le pouviez, il serait plus compliqué d'exiger deux passes de mise en page pour redimensionner les étiquettes multilignes. Cela risquait de ne pas correspondre à la façon dont les cellules à dimensionnement automatique veulent calculer toutes les tailles via un appel à systemLayoutSizeFittingSize.)

Toutefois, si vous souhaitez toujours créer une présentation ressemblant à une vue de table avec une disposition de flux, avec des cellules qui déterminent leur propre taille et répondent naturellement à la largeur de la vue de collection, cela est bien sûr possible. Il y a toujours le chemin en désordre. Je l’ai fait avec une "cellule de taille", c’est-à-dire une UICollectionViewCell non affichée que le contrôleur ne conserve que pour calculer la taille des cellules.

Cette approche comporte deux parties. La première partie consiste pour le délégué de la vue de collection à calculer la taille de cellule correcte en prenant en compte la largeur de la vue de collection et en utilisant la cellule de dimensionnement pour calculer la hauteur de la cellule.

Dans votre UICollectionViewDelegateFlowLayout, vous implémentez une méthode comme celle-ci:

func collectionView(collectionView: UICollectionView,
  layout collectionViewLayout: UICollectionViewLayout,
  sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
  // NOTE: here is where we say we want cells to use the width of the collection view
   let requiredWidth = collectionView.bounds.size.width

   // NOTE: here is where we ask our sizing cell to compute what height it needs
  let targetSize = CGSize(width: requiredWidth, height: 0)
  /// NOTE: populate the sizing cell's contents so it can compute accurately
  self.sizingCell.label.text = items[indexPath.row]
  let adequateSize = self.sizingCell.preferredLayoutSizeFittingSize(targetSize)
  return adequateSize
}

Ainsi, la vue de collection définira la largeur de la cellule en fonction de la vue de collection englobante, mais demandera ensuite à la cellule de dimensionnement de calculer la hauteur.

La deuxième partie consiste à obliger la cellule de dimensionnement à utiliser ses propres contraintes AL pour calculer la hauteur. Cela peut être plus difficile que cela ne devrait être, en raison de la façon dont les UILabel multilignes nécessitent en réalité un processus de mise en page en deux étapes. Le travail est fait dans la méthode preferredLayoutSizeFittingSize, qui ressemble à ceci:

 /*
 Computes the size the cell will need to be to fit within targetSize.

 targetSize should be used to pass in a width.

 the returned size will have the same width, and the height which is
 calculated by Auto Layout so that the contents of the cell (i.e., text in the label)
 can fit within that width.

 */
 func preferredLayoutSizeFittingSize(targetSize:CGSize) -> CGSize {

   // save original frame and preferredMaxLayoutWidth
   let originalFrame = self.frame
   let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth

   // assert: targetSize.width has the required width of the cell

   // step1: set the cell.frame to use that width
   var frame = self.frame
   frame.size = targetSize
   self.frame = frame

   // step2: layout the cell
   self.setNeedsLayout()
   self.layoutIfNeeded()
   self.label.preferredMaxLayoutWidth = self.label.bounds.size.width

   // assert: the label's bounds and preferredMaxLayoutWidth are set to the width required by the cell's width

   // step3: compute how tall the cell needs to be

   // this causes the cell to compute the height it needs, which it does by asking the 
   // label what height it needs to wrap within its current bounds (which we just set).
   let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

   // assert: computedSize has the needed height for the cell

   // Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes."
   let newSize = CGSize(width:targetSize.width,height:computedSize.height)

   // restore old frame and preferredMaxLayoutWidth
   self.frame = originalFrame
   self.label.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth

   return newSize
 }

(Ce code est adapté de l'exemple Apple de l'exemple de code de la session WWDC2014 dans "Vue de collection avancée".)

Quelques points à remarquer. Il utilise layoutIfNeeded () pour forcer la présentation de la cellule entière, afin de calculer et de définir la largeur de l'étiquette. Mais ça ne suffit pas. Je crois que vous devez également définir preferredMaxLayoutWidth pour que l'étiquette utilise cette largeur avec Mise en forme automatique. Et alors seulement, vous pourrez utiliser systemLayoutSizeFittingSize pour que la cellule calcule sa hauteur en tenant compte de l'étiquette.

Est-ce que j'aime cette approche? Non!! Cela semble trop complexe et cela fait deux fois la mise en page. Mais tant que les performances ne deviennent pas un problème, je préfère effectuer la mise en page deux fois au moment de l'exécution que de la définir deux fois dans le code, ce qui semble être la seule autre alternative.

Mon espoir est que les cellules à auto-dimensionnement finiront par fonctionner différemment et que cela deviendra beaucoup plus simple.

Exemple de projet en le montrant au travail.

Mais pourquoi ne pas simplement utiliser des cellules auto-dimensionnantes?

En théorie, les nouvelles installations d'iOS8 pour les "cellules auto-dimensionnées" devraient rendre cela inutile. Si vous avez défini une cellule avec Mise en page automatique (AL), la vue Collection devrait être suffisamment intelligente pour lui permettre de se redimensionner elle-même et de se présenter correctement. En pratique, je n’ai jamais vu d’exemple qui permette de travailler avec des étiquettes multilignes. Je pense que ceci est en partie parce que le mécanisme de cellule à dimensionnement automatique est toujours bogué.

Mais je parierais que c'est principalement à cause de la délicatesse habituelle de la mise en page automatique et des étiquettes, à savoir que les étiquettes UIL nécessitent un processus de mise en page en deux étapes. Je ne vois pas comment vous pouvez exécuter les deux étapes avec des cellules à taille automatique.

Et comme je l’ai dit, c’est vraiment un travail pour une mise en page différente. Une des caractéristiques essentielles de la mise en page de flux est de positionner les objets ayant une taille plutôt que de fixer une largeur et de leur permettre de choisir leur hauteur.

Et qu'en est-il de preferredLayoutAttributesFittingAttributes:?

Le preferredLayoutAttributesFittingAttributes: La méthode est un hareng rouge, je pense. C'est seulement là pour être utilisé avec le nouveau mécanisme de cellule auto-dimensionnement. Donc, ce n'est pas la solution tant que ce mécanisme n'est pas fiable.

Et quoi de neuf avec systemlayoutSizeFittingSize:?

Vous avez raison, les documents sont déroutants.

Les docs sur systemLayoutSizeFittingSize: et systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority: _ suggèrent tous les deux que vous ne devriez transmettre que UILayoutFittingCompressedSize et UILayoutFittingExpandedSize en tant que targetSize. Cependant, la signature de la méthode elle-même, les commentaires d'en-tête et le comportement des fonctions indiquent qu'elles répondent à la valeur exacte du paramètre targetSize.

En fait, si vous définissez le UICollectionViewFlowLayoutDelegate.estimatedItemSize, afin d'activer le nouveau mécanisme de cellule à auto-dimensionnement, cette valeur semble être transmise en tant que targetSize. Et UILabel.systemLayoutSizeFittingSize semble donner exactement les mêmes valeurs que UILabel.sizeThatFits. Ceci est suspect, étant donné que l'argument de systemLayoutSizeFittingSize est censé être une cible approximative et l'argument de sizeThatFits: est censé être une taille maximale circonscrite.

Davantage de ressources

Bien qu’il soit triste de penser qu’une telle exigence de routine devrait nécessiter des "ressources en matière de recherche", je pense que oui. De bons exemples et discussions sont:

132
algal

Il y a une manière plus propre de faire cela que certaines des autres réponses ici, et cela fonctionne bien. Il doit être performant (les vues de collection se chargent rapidement, pas de passes de mise en page automatiques inutiles, etc.), et ne possède aucun "nombre magique" comme une largeur de vue de collection fixe. Modification de la taille de la vue de collection, par exemple en rotation, puis en invalidant la mise en page devrait également fonctionner très bien.

1. Créez la sous-classe de présentation de flux suivante

class HorizontallyFlushCollectionViewFlowLayout: UICollectionViewFlowLayout {

    // Don't forget to use this class in your storyboard (or code, .xib etc)

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes
        guard let collectionView = collectionView else { return attributes }
        attributes?.bounds.size.width = collectionView.bounds.width - sectionInset.left - sectionInset.right
        return attributes
    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let allAttributes = super.layoutAttributesForElementsInRect(rect)
        return allAttributes?.flatMap { attributes in
            switch attributes.representedElementCategory {
            case .Cell: return layoutAttributesForItemAtIndexPath(attributes.indexPath)
            default: return attributes
            }
        }
    }
}

2. Enregistrez votre vue de collection pour le dimensionnement automatique

// The provided size should be a plausible estimate of the actual
// size. You can set your item size in your storyboard
// to a good estimate and use the code below. Otherwise,
// you can provide it manually too, e.g. CGSize(width: 100, height: 100)

flowLayout.estimatedItemSize = flowLayout.itemSize

3. Utilisez la largeur prédéfinie + la hauteur personnalisée dans votre sous-classe de cellules.

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutAttributes.bounds.size.height = systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    return layoutAttributes
}
49
Jordan Smith

Un moyen simple de le faire dans iOS 9 en quelques lignes de codes - l’exemple de chemin horizontal (en fixant sa hauteur à la hauteur de sa vue Collection):

Lancez votre présentation de flux de vue de collection avec un estimatedItemSize pour activer la cellule à dimensionnement automatique:

self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.estimatedItemSize = CGSizeMake(1, 1);

Implémentez le délégué de disposition de vue de collection (dans votre contrôleur de vue la plupart du temps), collectionView:layout:sizeForItemAtIndexPath:. L'objectif ici est de définir la hauteur fixe (ou largeur) sur la dimension Vue de collection. La valeur 10 peut être n'importe quoi, mais vous devriez lui donner une valeur qui ne casse pas les contraintes:

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return CGSizeMake(10, CGRectGetHeight(collectionView.bounds));
}

Remplacer votre cellule personnalisée preferredLayoutAttributesFittingAttributes: méthode, cette partie calcule la largeur de votre cellule dynamique en fonction de vos contraintes de mise en forme automatique et de la hauteur que vous venez de définir:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    UICollectionViewLayoutAttributes *attributes = [layoutAttributes copy];
    float desiredWidth = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].width;
    CGRect frame = attributes.frame;
    frame.size.width = desiredWidth;
    attributes.frame = frame;
    return attributes;
}
6
Tanguy G.

Essayez de fixer votre largeur dans les attributs de mise en page préférés:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [[super preferredLayoutAttributesFittingAttributes:layoutAttributes] copy];
    CGSize newSize = [self systemLayoutSizeFittingSize:CGSizeMake(FIXED_WIDTH,layoutAttributes.size) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGRect newFrame = attr.frame;
    newFrame.size.height = size.height;
    attr.frame = newFrame;
    return attr;
}

Naturellement, vous voulez également vous assurer que vous configurez votre mise en page correctement pour:

UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout;
flowLayout.estimatedItemSize = CGSizeMake(FIXED_WIDTH, estimatedHeight)];

Voici quelque chose que j'ai mis Github qui utilise des cellules de largeur constante et prend en charge le type dynamique afin que la hauteur des cellules soit mise à jour lorsque la taille de la police système change.

4
Daniel Galasko

OUI cela peut être fait en utilisant la disposition automatique par programme et en fixant des contraintes dans le storyboard ou xib. Vous devez ajouter une contrainte pour que la taille de la largeur reste constante et que la hauteur soit supérieure ou égale à.
http://www.thinkandbuild.it/learn-to-love-auto-layout-programmatically/

http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
J'espère que cela vous aidera et résoudra votre problème.

0
Dhaivat Vyas