UICollectionView automatise les cellules avec mise en page automatique
j'essaie d'obtenir la taille de moi-même UICollectionViewCells
travailler avec la mise en page automatique, mais je ne semble pas pouvoir obtenir les cellules pour se dimensionner au contenu. J'ai du mal à comprendre comment la taille de la cellule est mise à jour à partir du contenu de ce qui est à l'intérieur de contentView de la cellule.
Voici la configuration que j'ai essayé:
- Personnalisé
UICollectionViewCell
avec unUITextView
dans son contentView. - Scrolling pour le
UITextView
est désactivée. - la contrainte horizontale de contentView est:" H:|[_textView(320)]", i.e. le
UITextView
est épinglé à la gauche de la cellule avec une largeur explicite de 320. - la contrainte verticale de contentView est: "V:/-0-[_textView]", i.e. le
UITextView
épinglé au sommet de la cellule. - le
UITextView
a une contrainte de hauteur fixée à une constante que les rapportsUITextView
adapteront au texte.
voici à quoi il ressemble avec le fond de la cellule rouge, et le fond UITextView
Bleu:
j'ai mis le projet avec lequel j'ai joué sur GitHub ici .
13 réponses
mise à jour pour Swift 4
systemLayoutSizeFittingSize
renommé en systemLayoutSizeFitting
mise à jour pour iOS 9
après avoir vu ma solution GitHub casser sous iOS 9 j'ai finalement eu le temps d'examiner la question complètement. J'ai maintenant mis à jour le rapport pour inclure plusieurs exemples de différentes configurations pour les cellules auto dimensionnantes. Ma conclusion est que les cellules auto-dimensionnantes sont grandes en théorie, mais pratique. Un mot de prudence lors de la procédure d'auto dimensionnement des cellules.
TL; DR
Vérifier mon projet GitHub
les cellules auto-dimensionnantes ne sont supportées que par une disposition de flux, donc assurez-vous que c'est ce que vous utilisez.
il y a deux choses que vous devez configurer pour que les cellules auto dimensionnées fonctionnent.
1. estimatedItemSize
sur UICollectionViewFlowLayout
la disposition de flux deviendra dynamique dans la nature 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 calibrage sur votre cellule sous-classe
en 2 saveurs; mise en page automatique ou sur commande de preferredLayoutAttributesFittingAttributes
.
créer et configurer des cellules avec mise en page automatique
je ne vais pas à détail à ce sujet comme il ya un brillant SO post sur la configuration des contraintes pour une cellule. Il suffit de se méfier que Xcode 6 a cassé un tas de trucs avec iOS 7 donc, si vous supportez iOS 7, vous aurez besoin de faire des choses comme s'assurer que l'autoresizingMask est mis sur le contentView de la cellule et que les limites du contentView est défini comme les limites de la cellule quand la cellule est chargée (i.e. awakeFromNib
).
les choses que vous devez savoir est que votre cellule a besoin d'être plus sérieusement restreinte qu'une cellule de vue de Table. Par exemple, si vous voulez que votre largeur soit dynamique, alors votre cellule a besoin d'une contrainte de hauteur. De même, si vous voulez que la hauteur soit dynamique, vous aurez besoin d'une contrainte de largeur à votre cellule.
mettre en Œuvre preferredLayoutAttributesFittingAttributes
dans votre cellule personnalisé
lorsque cette fonction est appelée votre vue a déjà été configurée avec du contenu (i.e. cellForItem
a été appelé). En supposant que vos contraintes ont été 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 qui pourrait causer des accidents sur votre mise en œuvre si vous n'êtes pas prudent (Voir plus ici ). Lorsque vous implémentez preferredLayoutAttributesFittingAttributes
, vous devez vous assurer que vous ne modifiez le cadre de vos attributs qu'une seule fois. Si vous ne le faites pas la mise en page, appelez votre mise en œuvre indéfiniment et éventuellement se planter. Une solution est de mettre en cache la taille calculée dans votre cellule et d'invalider ceci chaque fois que vous réutilisez la cellule ou changez son contenu comme je l'ai fait avec la propriété isHeightCalculated
.
l'Expérience de votre mise en page
à ce stade, vous devriez avoir des cellules dynamiques "fonctionnelles" dans votre collection. Je n'ai pas encore trouvé la solution out-of-the-box suffisante au cours de mes tests alors n'hésitez pas à commenter si vous avoir. Il semble toujours que UITableView
gagne la bataille pour le dimensionnement dynamique IMHO.
mises en garde
soyez très conscient que si vous utilisez des cellules prototypes pour calculer l'estimatedItemSize - cela va casser si votre XIB utilise des classes de taille . La raison en est que lorsque vous chargez votre cellule à partir d'un XIB, sa classe de taille sera configurée avec Undefined
. Ce ne sera cassé Que sur iOS 8 et plus car sur iOS 7 la taille la classe va être chargé en fonction de l'appareil (iPad = Régulier, iPhone = Compact-Tout). Vous pouvez soit définir la taille estimatedItemSize sans charger le XIB, ou vous pouvez charger la cellule à partir du XIB, l'ajouter à la collection (cela définira la collecte de trait), effectuer la mise en page, puis le supprimer de la superview. Vous pouvez aussi faire en sorte que votre cellule supplante le getter traitCollection
et renvoie les traits appropriés. C'est à vous.
faites - moi savoir si j'ai manqué n'importe quoi, espère que j'ai aidé et bonne chance codage
quelques changements clés à la réponse de Daniel Galasko a réglé tous mes problèmes. Malheureusement, je n'ai pas assez de réputation pour commenter directement (encore).
à l'étape 1, lorsque vous utilisez la mise en page automatique, il vous suffit d'ajouter un uivi monoparental à la cellule. Tout ce qui est dans la cellule doit être un sous-vue du parent. Qui a répondu à tous mes problèmes. Alors que XCode ajoute ceci pour les cellules de visualisation utilisables automatiquement, il ne le fait pas (mais il devrait le faire) pour les cellules de visualisation Uicollection. Selon le docs :
pour configurer l'apparence de votre cellule, ajoutez les vues nécessaires pour présenter le contenu de l'élément de données comme des sous-vues de la vue dans la propriété contentView. N'ajoutez pas directement de subviews à la cellule elle-même.
puis sauter entièrement l'étape 3. Il n'est pas nécessaire.
dans iOS10 il y a une nouvelle constante appelée UICollectionViewFlowLayoutAutomaticSize
, donc à la place:
self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)
vous pouvez utiliser ceci:
self.flowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
il a une meilleure performance en particulier lorsque les cellules dans votre vue collection a wid constante
Dans iOS 10+ c'est très simple en 2 étapes.
-
assurez-vous que tous vos contenus de cellules sont placés dans un uivi (ou à l'intérieur d'un descendant D'uivi comme UIStackView qui simplifie autolayout beaucoup). Tout comme avec les cellules de vue Uitable redimensionnement dynamique, toute la hiérarchie de la vue doit avoir des contraintes configurées, du conteneur le plus externe à la vue la plus interne. Cela inclut les contraintes entre les Uicollection viewcell and the immediate childview
-
instruisez le flowlayout de votre UICollectionView à la taille automatique
yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
-
Ajouter flowLayout sur viewDidLoad()
override func viewDidLoad() { super.viewDidLoad() if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize(width: 1, height:1) } }
-
aussi, définissez un uivi comme mainContainer pour votre cellule et ajoutez toutes les vues nécessaires à l'intérieur de celle-ci.
-
reportez-vous à ce tutoriel impressionnant pour plus de référence: UICollectionView avec redimensionnement automatique de la cellule à l'aide de mise en forme automatique dans iOS 9 & 10
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
j'ai fait une hauteur de cellule dynamique de vue collection. Voici git hub repo .
et, creuser pourquoi preferredLayoutAttributesFittingattributes est appelé plus d'une fois. En réalité, il sera appelé au moins 3 fois.
l'image du journal de bord de la console :
1st preferredlayouttattributesfittingattributes :
(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.cadre.taille.la hauteur est l'état actuel 57.5 .
2nd Preferred layouttattributesfittingattributes :
(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 de la cellule a changé en 534.5 comme prévu. Mais, la vue de la collection toujours hauteur zéro.
de la 3ème 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 la hauteur de vue de la collection a été changée de 0 à 477 .
le comportement est similaire à handle scroll:
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. Donc j'ai 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;
causera l'appel système boucle infinie et le plantage de L'application.
N'importe quelle taille changée, il validera toutes les cellules' preferredLayoutAttributesFittingattributes encore et encore jusqu'à chaque position des cellules (I. e frames) ne sont plus un changement.
À qui il peut aider,
j'ai eu ce terrible accident si estimatedItemSize
était mis. Même si j'ai retourné 0 dans numberOfItemsInSection
. Par conséquent, les cellules elles-mêmes et leur disposition automatique n'ont pas été la cause de l'accident... Le collectionView s'est écrasé, même vide, juste parce que estimatedItemSize
était réglé pour l'auto-dimensionnement.
dans mon cas j'ai réorganisé mon projet, d'un contrôleur contenant une vue de collection à un contrôleur de vue de collection, et il travaillé.
Aller à la figure.
l'exemple de méthode ci-dessus ne compile pas. Voici une version corrigée (mais non testée quant à savoir si elle 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
}
si vous mettez en œuvre la méthode Uicollection Viewdelegateflowlayout:
- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath*)indexPath
quand vous appelez collectionview performBatchUpdates:completion:
, la hauteur de taille utilisera sizeForItemAtIndexPath
au lieu de
preferredLayoutAttributesFittingAttributes
.
le processus de rendu de performBatchUpdates:completion
passera par la méthode preferredLayoutAttributesFittingAttributes
mais il ignore vos changements.
Update Plus d'information:
-
si vous utilisez
flowLayout.estimatedItemSize
, suggérez d'utiliser iOS8.3 version ultérieure. Avant iOS8.3, Il s'écrasera[super layoutAttributesForElementsInRect:rect];
. Le message d'erreur est*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
-
en Second lieu, dans iOS8.la version x,
flowLayout.estimatedItemSize
va causer le réglage de section inset différent n'a pas fonctionné. c'est à dire de la fonction:(UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:
.
pour tous ceux qui ont tout essayé sans succès, c'est la seule chose qui l'a fait fonctionner pour moi. Pour les étiquettes multilignes à l'intérieur de la cellule, essayez d'ajouter cette ligne magique:
label.preferredMaxLayoutWidth = 200
plus d'informations: ici
santé!
j'ai essayé d'utiliser estimatedItemSize
mais il y avait un tas de bogues lors de l'insertion et de la suppression des cellules si le estimatedItemSize
n'était pas exactement égale à la hauteur de la cellule. j'ai arrêté de régler estimatedItemSize
et j'ai implémenté dynamic cell en utilisant un prototype de cellule. voici comment faire:
créer ce protocole:
protocol SizeableCollectionViewCell {
func fittedSize(forConstrainedSize size: CGSize)->CGSize
}
mettre en œuvre ce protocole dans votre coutume UICollectionViewCell
:
class YourCustomCollectionViewCell: UICollectionViewCell, SizeableCollectionViewCell {
@IBOutlet private var mTitle: UILabel!
@IBOutlet private var mDescription: UILabel!
@IBOutlet private var mContentView: UIView!
@IBOutlet private var mTitleTopConstraint: NSLayoutConstraint!
@IBOutlet private var mDesciptionBottomConstraint: NSLayoutConstraint!
func fittedSize(forConstrainedSize size: CGSize)->CGSize {
let fittedSize: CGSize!
//if height is greatest value, then it's dynamic, so it must be calculated
if size.height == CGFLoat.greatestFiniteMagnitude {
var height: CGFloat = 0
/*now here's where you want to add all the heights up of your views.
apple provides a method called sizeThatFits(size:), but it's not
implemented by default; except for some concrete subclasses such
as UILabel, UIButton, etc. search to see if the classes you use implement
it. here's how it would be used:
*/
height += mTitle.sizeThatFits(size).height
height += mDescription.sizeThatFits(size).height
height += mCustomView.sizeThatFits(size).height //you'll have to implement this in your custom view
//anything that takes up height in the cell has to be included, including top/bottom margin constraints
height += mTitleTopConstraint.constant
height += mDescriptionBottomConstraint.constant
fittedSize = CGSize(width: size.width, height: height)
}
//else width is greatest value, if not, you did something wrong
else {
//do the same thing that's done for height but with width, remember to include leading/trailing margins in calculations
}
return fittedSize
}
}
faites votre contrôleur de se conformer à UICollectionViewDelegateFlowLayout
, et, en ce domaine:
class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout {
private var mCustomCellPrototype = UINib(nibName: <name of the nib file for your custom collectionviewcell>, bundle: nil).instantiate(withOwner: nil, options: nil).first as! SizeableCollectionViewCell
}
il sera utilisé comme une cellule prototype pour lier des données à et ensuite déterminer comment ces données ont affecté la dimension que vous voulez être dynamique
enfin, le UICollectionViewDelegateFlowLayout's
collectionView(:layout:sizeForItemAt:)
doit être mis en œuvre:
class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
private var mDataSource: [CustomModel]
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath)->CGSize {
//bind the prototype cell with the data that corresponds to this index path
mCustomCellPrototype.bind(model: mDataSource[indexPath.row]) //this is the same method you would use to reconfigure the cells that you dequeue in collectionView(:cellForItemAt:). i'm calling it bind
//define the dimension you want constrained
let width = UIScreen.main.bounds.size.width - 20 //the width you want your cells to be
let height = CGFloat.greatestFiniteMagnitude //height has the greatest finite magnitude, so in this code, that means it will be dynamic
let constrainedSize = CGSize(width: width, height: height)
//determine the size the cell will be given this data and return it
return mCustomCellPrototype.fittedSize(forConstrainedSize: constrainedSize)
}
}
et c'est tout. Retourner la taille de la cellule dans collectionView(:layout:sizeForItemAt:)
de cette façon m'empêchant d'avoir à utiliser estimatedItemSize
, et l'insertion et la suppression de cellules fonctionne parfaitement.