Solución:
Con Swift 5 y iOS 12.3, puede usar uno de los 4 implementaciones siguientes para establecer el número de elementos por fila en su UICollectionView
mientras se gestionan las inserciones y los cambios de tamaño (incluida la rotación).
# 1. Subclasificación UICollectionViewFlowLayout
y usando UICollectionViewFlowLayout
‘s itemSize
propiedad
ColumnFlowLayout.swift:
import UIKit
class ColumnFlowLayout: UICollectionViewFlowLayout
let cellsPerRow: Int
init(cellsPerRow: Int, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero)
self.cellsPerRow = cellsPerRow
super.init()
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
required init?(coder aDecoder: NSCoder)
fatalError("init(coder:) has not been implemented")
override func prepare()
super.prepare()
guard let collectionView = collectionView else return
let marginsAndInsets = sectionInset.left + sectionInset.right + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + minimumInteritemSpacing * CGFloat(cellsPerRow - 1)
let itemWidth = ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)
itemSize = CGSize(width: itemWidth, height: itemWidth)
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext
let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
CollectionViewController.swift:
import UIKit
class CollectionViewController: UICollectionViewController
let columnLayout = ColumnFlowLayout(
cellsPerRow: 5,
minimumInteritemSpacing: 10,
minimumLineSpacing: 10,
sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
)
override func viewDidLoad()
super.viewDidLoad()
collectionView?.collectionViewLayout = columnLayout
collectionView?.contentInsetAdjustmentBehavior = .always
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
return 59
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.backgroundColor = UIColor.orange
return cell
# 2. Utilizando UICollectionViewFlowLayout
‘s itemSize
método
import UIKit
class CollectionViewController: UICollectionViewController
let margin: CGFloat = 10
let cellsPerRow = 5
override func viewDidLoad()
super.viewDidLoad()
guard let collectionView = collectionView, let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else return
flowLayout.minimumInteritemSpacing = margin
flowLayout.minimumLineSpacing = margin
flowLayout.sectionInset = UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)
collectionView.contentInsetAdjustmentBehavior = .always
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
override func viewWillLayoutSubviews()
guard let collectionView = collectionView, let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else return
let marginsAndInsets = flowLayout.sectionInset.left + flowLayout.sectionInset.right + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + flowLayout.minimumInteritemSpacing * CGFloat(cellsPerRow - 1)
let itemWidth = ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)
flowLayout.itemSize = CGSize(width: itemWidth, height: itemWidth)
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
return 59
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.backgroundColor = UIColor.orange
return cell
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
collectionView?.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
# 3. Utilizando UICollectionViewDelegateFlowLayout
‘s collectionView(_:layout:sizeForItemAt:)
método
import UIKit
class CollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout
let inset: CGFloat = 10
let minimumLineSpacing: CGFloat = 10
let minimumInteritemSpacing: CGFloat = 10
let cellsPerRow = 5
override func viewDidLoad()
super.viewDidLoad()
collectionView?.contentInsetAdjustmentBehavior = .always
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
return UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset)
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat
return minimumLineSpacing
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat
return minimumInteritemSpacing
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
let marginsAndInsets = inset * 2 + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + minimumInteritemSpacing * CGFloat(cellsPerRow - 1)
let itemWidth = ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)
return CGSize(width: itemWidth, height: itemWidth)
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
return 59
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.backgroundColor = UIColor.orange
return cell
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
collectionView?.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
# 4. Subclasificación UICollectionViewFlowLayout
y usando UICollectionViewFlowLayout
‘s estimatedItemSize
propiedad
CollectionViewController.swift:
import UIKit
class CollectionViewController: UICollectionViewController
let items = [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Lorem ipsum dolor sit amet, consectetur.",
"Lorem ipsum dolor sit amet.",
"Lorem ipsum dolor sit amet, consectetur.",
"Lorem ipsum dolor sit amet, consectetur adipiscing.",
"Lorem ipsum.",
"Lorem ipsum dolor sit amet.",
"Lorem ipsum dolor sit.",
"Lorem ipsum dolor sit amet, consectetur adipiscing.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.",
"Lorem ipsum dolor sit amet, consectetur."
]
let columnLayout = FlowLayout(
cellsPerRow: 3,
minimumInteritemSpacing: 10,
minimumLineSpacing: 10,
sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
)
override func viewDidLoad()
super.viewDidLoad()
collectionView?.collectionViewLayout = columnLayout
collectionView?.contentInsetAdjustmentBehavior = .always
collectionView?.register(Cell.self, forCellWithReuseIdentifier: "Cell")
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
return items.count
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
cell.label.text = items[indexPath.row]
return cell
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
collectionView?.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
FlowLayout.swift:
import UIKit
class FlowLayout: UICollectionViewFlowLayout
let cellsPerRow: Int
required init(cellsPerRow: Int = 1, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero)
self.cellsPerRow = cellsPerRow
super.init()
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
required init?(coder aDecoder: NSCoder)
fatalError("init(coder:) has not been implemented")
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath) else return nil
guard let collectionView = collectionView else return layoutAttributes
let marginsAndInsets = collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + sectionInset.left + sectionInset.right + minimumInteritemSpacing * CGFloat(cellsPerRow - 1)
layoutAttributes.bounds.size.width = ((collectionView.bounds.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)
return layoutAttributes
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
let superLayoutAttributes = super.layoutAttributesForElements(in: rect)!.map $0.copy() as! UICollectionViewLayoutAttributes
guard scrollDirection == .vertical else return superLayoutAttributes
let layoutAttributes = superLayoutAttributes.compactMap layoutAttribute in
return layoutAttribute.representedElementCategory == .cell ? layoutAttributesForItem(at: layoutAttribute.indexPath) : layoutAttribute
// (optional) Uncomment to top align cells that are on the same line
/*
let cellAttributes = layoutAttributes.filter( $0.representedElementCategory == .cell )
for (_, attributes) in Dictionary(grouping: cellAttributes, by: ($0.center.y / 10).rounded(.up) * 10 )
guard let max = attributes.max(by: $0.size.height < $1.size.height ) else continue
for attribute in attributes where attribute.size.height != max.size.height
attribute.frame.origin.y = max.frame.origin.y
*/
// (optional) Uncomment to bottom align cells that are on the same line
/*
let cellAttributes = layoutAttributes.filter( $0.representedElementCategory == .cell )
for (_, attributes) in Dictionary(grouping: cellAttributes, by: ($0.center.y / 10).rounded(.up) * 10 )
guard let max = attributes.max(by: $0.size.height < $1.size.height ) else continue
for attribute in attributes where attribute.size.height != max.size.height
attribute.frame.origin.y += max.frame.maxY - attribute.frame.maxY
*/
return layoutAttributes
Cell.swift:
import UIKit
class Cell: UICollectionViewCell
let label = UILabel()
override init(frame: CGRect)
super.init(frame: frame)
label.numberOfLines = 0
backgroundColor = .orange
contentView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
required init?(coder aDecoder: NSCoder)
fatalError("init(coder:) has not been implemented")
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
layoutIfNeeded()
label.preferredMaxLayoutWidth = label.bounds.size.width
layoutAttributes.bounds.size.height = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
return layoutAttributes
// Alternative implementation
/*
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
label.preferredMaxLayoutWidth = layoutAttributes.size.width - contentView.layoutMargins.left - contentView.layoutMargins.right
layoutAttributes.bounds.size.height = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
return layoutAttributes
*/
CollectionViews son muy poderosos y tienen un precio. Muchas y muchas opciones. Como dijo omz:
hay varias formas de cambiar el número de columnas
Sugeriría implementar el
Protocolo, que le da acceso a los siguientes métodos en los que puede tener un mayor control sobre el diseño de su UICollectionView
, sin la necesidad de subclasificarlo:
collectionView:layout:insetForSectionAtIndex:
collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
collectionView:layout:minimumLineSpacingForSectionAtIndex:
collectionView:layout:referenceSizeForFooterInSection:
collectionView:layout:referenceSizeForHeaderInSection:
collectionView:layout:sizeForItemAtIndexPath:
Además, la implementación del siguiente método obligará a su UICollectionView a actualizar su diseño en un cambio de orientación: (digamos que desea cambiar el tamaño de las celdas para el paisaje y estirarlas)
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
duration:(NSTimeInterval)duration
[self.myCollectionView.collectionViewLayout invalidateLayout];
Además, aquí hay 2 tutoriales realmente buenos sobre UICollectionViews
:
http://www.raywenderlich.com/22324/beginning-uicollectionview-in-ios-6-part-12
http://skeuo.com/uicollectionview-custom-layout-tutorial
Yo implementé UICollectionViewDelegateFlowLayout
en mi UICollectionViewController
y anular el método responsable de determinar el tamaño de la celda. Luego tomé el ancho de la pantalla y lo dividí con el requisito de mi columna. Por ejemplo, quería tener 3 columnas en cada tamaño de pantalla. Así que así es como se ve mi código:
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat screenWidth = screenRect.size.width;
float cellWidth = screenWidth / 3.0; //Replace the divisor with the column count requirement. Make sure to have it in float.
CGSize size = CGSizeMake(cellWidth, cellWidth);
return size;