ios - automatic - UICollectionView Celdas de tamaño automático con diseño automático
collectionview with autolayout (15)
Actualizado para Swift 5
preferredLayoutAttributesFittingAttributes
renombrado a
preferredLayoutAttributesFitting
y usar el tamaño automático
Actualizado para Swift 4
systemLayoutSizeFittingSize
renombrado a
systemLayoutSizeFitting
Actualizado para iOS 9
Después de ver que mi solución GitHub se rompió en iOS 9, finalmente tuve el tiempo para investigar el problema por completo. Ahora he actualizado el repositorio para incluir varios ejemplos de diferentes configuraciones para celdas de tamaño propio. Mi conclusión es que las células auto dimensionantes son geniales en teoría pero desordenadas en la práctica. Una palabra de precaución al proceder con células auto dimensionantes.
TL; DR
Mira mi proyecto GitHub
Las celdas de tamaño automático solo son compatibles con el diseño de flujo, así que asegúrese de que eso es lo que está utilizando.
Hay dos cosas que debe configurar para que funcionen las celdas de tamaño automático.
1. Establezca
estimatedItemSize
en
UICollectionViewFlowLayout
El diseño del flujo se volverá dinámico en la naturaleza una vez que establezca la propiedad
estimatedItemSize
.
self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
2. Agregue soporte para dimensionar en su subclase de celda
Esto viene en 2 sabores;
Diseño automático
o
anulación personalizada de
preferredLayoutAttributesFittingAttributes
.
Crear y configurar celdas con diseño automático
No entraré en detalles sobre esto, ya que hay una
publicación SO
brillante sobre la configuración de restricciones para una celda.
Solo tenga cuidado de que
Xcode 6 rompió
un montón de cosas con iOS 7, por lo que, si es compatible con iOS 7, deberá hacer cosas como asegurarse de que autoresizingMask esté configurado en contentView de la celda y que los límites de contentView estén configurados como los límites de la celda cuando la celda se carga (es decir,
awakeFromNib
).
Lo que debe tener en cuenta es que su celda debe estar más seriamente restringida que una celda de vista de tabla. Por ejemplo, si desea que su ancho sea dinámico, su celda necesita una restricción de altura. Del mismo modo, si desea que la altura sea dinámica, necesitará una restricción de ancho para su celda.
Implemente
preferredLayoutAttributesFittingAttributes
en su celda personalizada
Cuando se llama a esta función, su vista ya se ha configurado con contenido (es decir, se ha llamado a
cellForItem
).
Suponiendo que sus restricciones se hayan establecido adecuadamente, podría tener una implementación como esta:
//forces the system to do one layout pass
var isHeightCalculated: Bool = false
override func preferredLayoutAttributesFitting(_ 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
}
NOTA:
En iOS 9, el comportamiento cambió un poco y podría causar fallas en su implementación si no tiene cuidado (vea más
here
).
Cuando implemente
preferredLayoutAttributesFittingAttributes
, debe asegurarse de que solo cambie el marco de sus atributos de diseño una vez.
Si no hace esto, el diseño llamará a su implementación indefinidamente y finalmente se bloqueará.
Una solución es almacenar en caché el tamaño calculado en su celda e invalidar esto cada vez que reutilice la celda o cambie su contenido como lo hice con la propiedad
isHeightCalculated
.
Experimenta tu diseño
En este punto, debería tener celdas dinámicas "funcionales" en su collectionView.
Todavía no he encontrado la solución inmediata suficiente durante mis pruebas, así que siéntase libre de comentar.
Todavía se siente como
UITableView
gana la batalla por el tamaño dinámico en mi humilde opinión.
Advertencias
Tenga en cuenta que si está utilizando celdas prototipo para calcular el
tamaño
estimado del artículo, esto se romperá si su
XIB usa clases de tamaño
.
La razón de esto es que cuando carga su celda desde un XIB, su clase de tamaño se configurará con
Undefined
.
Esto solo se romperá en iOS 8 y superior, ya que en iOS 7 la clase de tamaño se cargará en función del dispositivo (iPad = Regular-Any, iPhone = Compact-Any).
Puede establecer el tamaño estimado del artículo sin cargar el XIB, o puede cargar la celda desde el XIB, agregarlo a la colecciónView (esto establecerá la colección de caracteres), realizar el diseño y luego eliminarlo de la supervista.
Alternativamente, también puede hacer que su celda anule el captador
traitCollection
y devuelva los rasgos apropiados.
Tu decides.
Avíseme si me perdí algo, espero haber ayudado y buena suerte codificando
Estoy tratando de hacer que
UICollectionViewCells
funcione con Auto Layout, pero parece que no puedo hacer que las celdas se
UICollectionViewCells
al contenido.
Tengo problemas para entender cómo se actualiza el tamaño de la celda a partir del contenido de lo que está dentro de contentView de la celda.
Aquí está la configuración que he probado:
-
UICollectionViewCell
personalizado con unUITextView
en su contentView. -
El desplazamiento hacia
UITextView
está deshabilitado. -
La restricción horizontal de contentView es: "H: | [_textView (320)]", es decir,
UITextView
estáUITextView
a la izquierda de la celda con un ancho explícito de 320. -
La restricción vertical de contentView es: "V: | -0 - [_ textView]", es decir,
UITextView
fijado en la parte superior de la celda. -
UITextView
tiene una restricción de altura establecida en una constante que los informes deUITextView
se ajustarán al texto.
Esto es lo que parece con el fondo de la celda establecido en rojo y el fondo
UITextView
establecido en Azul:
Puse el proyecto con el que he estado jugando en GitHub here .
contentView misterio de anclaje:
En un caso extraño esto
contentView.translatesAutoresizingMaskIntoConstraints = false
no funcionaría. Se agregaron cuatro anclas explícitas a contentView y funcionó.
class AnnoyingCell: UICollectionViewCell {
@IBOutlet var word: UILabel!
override init(frame: CGRect) {
super.init(frame: frame); common() }
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder); common() }
private func common() {
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leftAnchor.constraint(equalTo: leftAnchor),
contentView.rightAnchor.constraint(equalTo: rightAnchor),
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
y como siempre
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
en
YourLayout: UICollectionViewFlowLayout
¿Quién sabe? Podría ayudar a alguien.
Crédito
https://www.vadimbulavin.com/collection-view-cells-self-sizing/
Tropecé con la punta allí, nunca lo vi en ningún otro lugar en todos los artículos de 1000 sobre esto.
-
Agregue flowLayout en viewDidLoad ()
override func viewDidLoad() { super.viewDidLoad() if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize(width: 1, height:1) } }
-
Además, configure una UIView como mainContainer para su celda y agregue todas las vistas requeridas dentro de ella.
-
Consulte este increíble tutorial alucinante para obtener más información: UICollectionView con celda de tamaño automático utilizando autolayout en iOS 9 y 10
A quien pueda ayudar,
Tuve ese accidente desagradable si se estableció
estimatedItemSize
.
Incluso si
numberOfItemsInSection
0 en
numberOfItemsInSection
.
Por lo tanto, las celdas en sí mismas y su diseño automático no fueron la causa del bloqueo ... El collectionView simplemente se bloqueó, incluso cuando estaba vacío, solo porque
estimatedItemSize
estaba configurada para auto-dimensionamiento.
En mi caso reorganicé mi proyecto, desde un controlador que contiene un collectionView a un collectionViewController, y funcionó.
Imagínate.
Actualiza más información:
-
Si usa
flowLayout.estimatedItemSize
, sugiera usar la versión posterior de iOS8.3. Antes de iOS8.3, se bloqueará[super layoutAttributesForElementsInRect:rect];
. El mensaje de error es*** Terminating app due to uncaught exception ''NSInvalidArgumentException'', reason: ''*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil''
-
En segundo lugar, en la versión iOS8.x,
flowLayout.estimatedItemSize
hará que la configuración de inserción de sección diferente no funcione. es decir, función:(UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:
Además de las respuestas anteriores,
Solo asegúrese de establecer la propiedad estimadaItemSize de UICollectionViewFlowLayout en algún tamaño y no implemente el método de delegado sizeForItem: atIndexPath .
Eso es.
Algunos cambios clave en la respuesta de Daniel Galasko solucionaron todos mis problemas. Desafortunadamente, no tengo suficiente reputación para comentar directamente (todavía).
En el paso 1, cuando use el diseño automático, simplemente agregue una vista UIView principal única a la celda. TODO dentro de la celda debe ser una subvista del padre. Eso respondió a todos mis problemas. Si bien Xcode agrega esto para UITableViewCells automáticamente, no lo hace (pero debería) para UICollectionViewCells. Según los docs :
Para configurar la apariencia de su celda, agregue las vistas necesarias para presentar el contenido del elemento de datos como subvistas a la vista en la propiedad contentView. No agregue directamente subvistas a la celda misma.
Luego omita el paso 3 por completo. No es necesario
Después de luchar con esto durante algún tiempo, noté que el cambio de tamaño no funciona para UITextViews si no deshabilita el desplazamiento:
let textView = UITextView()
textView.scrollEnabled = false
El método de ejemplo anterior no se compila. Aquí hay una versión corregida (pero no comprobada si funciona o no).
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
}
En iOS 10+, este es un proceso muy simple de 2 pasos.
-
Asegúrese de que todos los contenidos de sus celdas se coloquen dentro de una sola UIView (o dentro de un descendiente de UIView como UIStackView, lo que simplifica mucho la distribución automática). Al igual que con el cambio de tamaño dinámico de UITableViewCells, toda la jerarquía de vistas debe tener restricciones configuradas, desde el contenedor más externo hasta la vista más interna. Eso incluye restricciones entre UICollectionViewCell y la vista secundaria inmediata
-
Indique al flowlayout de su UICollectionView que dimensione automáticamente
yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
En iOS10 hay una nueva constante llamada
UICollectionViewFlowLayout.automaticSize
(anteriormente
UICollectionViewFlowLayoutAutomaticSize
), así que en su lugar:
self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)
puedes usar esto:
self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
Tiene un mejor rendimiento, especialmente cuando las celdas en su vista de colección tienen wid constante
Acceso al diseño del flujo:
override func viewDidLoad() {
super.viewDidLoad()
if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
Swift 5 actualizado:
override func viewDidLoad() {
super.viewDidLoad()
if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
}
}
Hice una altura de celda dinámica de la vista de colección. Aquí está el repositorio de git hub .
Y, descubra por qué se llama más preferido una vez a PreferidLayoutAttributesFittingAttributes. En realidad, se llamará al menos 3 veces.
La imagen de registro de la consola:
1er preferidoLayoutAttributesFittingAttributes :
(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);
El layoutAttributes.frame.size.height es el estado actual 57.5 .
2do preferidoLayoutAttributesFittingAttributes :
(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 altura del marco de la celda cambió a 534.5 como se esperaba. Pero, la vista de colección sigue siendo de altura cero.
3er preferidoLayoutAttributesFittingAttributes :
(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);
Puede ver que la altura de la vista de colección cambió de 0 a 477 .
El comportamiento es similar a manejar desplazamiento:
1. Before self-sizing cell
2. Validated self-sizing cell again after other cells recalculated.
3. Did changed self-sizing cell
Al principio, pensé que este método solo llamaba una vez. Así que codifiqué lo siguiente:
CGRect frame = layoutAttributes.frame;
frame.size.height = frame.size.height + self.collectionView.contentSize.height;
UICollectionViewLayoutAttributes* newAttributes = [layoutAttributes copy];
newAttributes.frame = frame;
return newAttributes;
Esta línea:
frame.size.height = frame.size.height + self.collectionView.contentSize.height;
hará que el sistema llame al bucle infinito y se bloquee la aplicación
Cualquier tamaño cambiado, validará todas las celdas ''PreferredAttributesFittingAttributes'' de todas las celdas una y otra vez hasta que las posiciones de cada celda (es decir, marcos) ya no cambien.
Intenté usar
estimatedItemSize
pero hubo un montón de errores al insertar y eliminar celdas si el
estimatedItemSize
no era exactamente igual a la altura de la celda.
Dejé de establecer el tamaño de artículo
estimatedItemSize
e implementé celdas dinámicas mediante el uso de una celda prototipo.
así es como se hace:
crea este protocolo:
protocol SizeableCollectionViewCell {
func fittedSize(forConstrainedSize size: CGSize)->CGSize
}
implemente este protocolo en su
UICollectionViewCell
personalizada:
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
}
}
ahora haga que su controlador se ajuste a
UICollectionViewDelegateFlowLayout
y, en él, tenga este campo:
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
}
se usará como una celda prototipo para enlazar datos y luego determinar cómo esos datos afectaron la dimensión en la que desea ser dinámico
finalmente, se debe
UICollectionViewDelegateFlowLayout''s
el
UICollectionViewDelegateFlowLayout''s
collectionView(:layout:sizeForItemAt:)
:
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)
}
}
y eso es.
Devolver el tamaño de la celda en
collectionView(:layout:sizeForItemAt:)
de esta manera evitando que tenga que usar el tamaño de artículo
estimatedItemSize
, e insertar y eliminar celdas funciona perfectamente.
Para cualquiera que haya intentado todo sin suerte, esto es lo único que lo hizo funcionar para mí. Para las etiquetas multilínea dentro de la celda, intente agregar esta línea mágica:
label.preferredMaxLayoutWidth = 200
Más información: here
¡Salud!
Si implementa el método UICollectionViewDelegateFlowLayout:
- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath*)indexPath
Cuando llame a
collectionview performBatchUpdates:completion:
la altura del tamaño usará
sizeForItemAtIndexPath
lugar de
preferredLayoutAttributesFittingAttributes
.
El proceso de representación de
performBatchUpdates:completion
pasará por el método
preferredLayoutAttributesFittingAttributes
pero ignorará sus cambios.