列数

我刚开始学习 UICollectionView。我想知道是否有人知道如何指定 Collectionview 中的列数。默认设置为3(iPhone/签名)。我看了文件,似乎找不到一个简明的答案。

161204 次浏览

Because UICollectionView is so flexible, there are multiple ways you could change the number of columns, depending on the kind of layout you use.

The UICollectionViewFlowLayout (which is probably what you're working with) doesn't specify a number of columns directly (because it depends on the view size/orientation). The easiest way to change it would be to set the itemSize property and/or minimumInteritemSpacing/minimumLineSpacing.

CollectionViews are very powerful, and they come at a price. Lots, and lots of options. As omz said:

there are multiple ways you could change the number of columns

I'd suggest implementing the <UICollectionViewDelegateFlowLayout> Protocol, giving you access to the following methods in which you can have greater control over the layout of your UICollectionView, without the need for subclassing it:

  • collectionView:layout:insetForSectionAtIndex:
  • collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
  • collectionView:layout:minimumLineSpacingForSectionAtIndex:
  • collectionView:layout:referenceSizeForFooterInSection:
  • collectionView:layout:referenceSizeForHeaderInSection:
  • collectionView:layout:sizeForItemAtIndexPath:

Also, implementing the following method will force your UICollectionView to update it's layout on an orientation change: (say you wanted to re-size the cells for landscape and make them stretch)

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
duration:(NSTimeInterval)duration{


[self.myCollectionView.collectionViewLayout invalidateLayout];
}

Additionally, here are 2 really good tutorials on UICollectionViews:

http://www.raywenderlich.com/22324/beginning-uicollectionview-in-ios-6-part-12

http://skeuo.com/uicollectionview-custom-layout-tutorial

Its all about layout you want to draw. You can create custom class inheriting from UICollectionViewFlowLayout. Currently there is not any direct method to set columns. If you want to achieve this kind of functionality you need to do it manually. You need to handle it in your custom flow layout class.

Now question will arise how you will do it? If you dont want to disturb cell frame you can adjust

 collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
collectionView:layout:minimumLineSpacingForSectionAtIndex:

Another way is provide you own positions of cells. By overriding below two methods, which will get called during your layout formation.

  - (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path

UICollectionViewLayoutAttributes is class which will deal with cell position, frame, Zindex etc

I implemented UICollectionViewDelegateFlowLayout on my UICollectionViewController and override the method responsible for determining the size of the Cell. I then took the screen width and divided it with my column requirement. For example, I wanted to have 3 columns on each screen size. So here's what my code looks like -

- (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;
}

Updated to Swift 3:

Instead of the flow layout, I prefer using custom layout for specific column number and row number. Because:

  1. It can be dragged horizontally if column number is very big.
  2. It is more acceptable logically because of using column and row.

Normal cell and Header cell: (Add UILabel as a IBOutlet to your xib):

class CollectionViewCell: UICollectionViewCell {


@IBOutlet weak var label: UILabel!


override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.backgroundColor = UIColor.black
label.textColor = UIColor.white
}
}


class CollectionViewHeadCell: UICollectionViewCell {


@IBOutlet weak var label: UILabel!


override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.backgroundColor = UIColor.darkGray
label.textColor = UIColor.white
}
}

Custom layout:

let cellHeight: CGFloat = 100
let cellWidth: CGFloat = 100


class CustomCollectionViewLayout: UICollectionViewLayout {
private var numberOfColumns: Int!
private var numberOfRows: Int!


// It is two dimension array of itemAttributes
private var itemAttributes = [[UICollectionViewLayoutAttributes]]()
// It is one dimension of itemAttributes
private var cache = [UICollectionViewLayoutAttributes]()


override func prepare() {
if self.cache.isEmpty {


self.numberOfColumns = self.collectionView?.numberOfItems(inSection: 0)
self.numberOfRows = self.collectionView?.numberOfSections


// Dynamically change cellWidth if total cell width is smaller than whole bounds
/* if (self.collectionView?.bounds.size.width)!/CGFloat(self.numberOfColumns) > cellWidth {
self.cellWidth = (self.collectionView?.bounds.size.width)!/CGFloat(self.numberOfColumns)
}
*/
for row in 0..<self.numberOfRows {
var row_temp = [UICollectionViewLayoutAttributes]()
for column in 0..<self.numberOfColumns {


let indexPath = NSIndexPath(item: column, section: row)


let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
attributes.frame = CGRect(x: cellWidth*CGFloat(column), y: cellHeight*CGFloat(row), width: cellWidth, height: cellHeight)


row_temp.append(attributes)


self.cache.append(attributes)
}
self.itemAttributes.append(row_temp)
}
}
}
override var collectionViewContentSize: CGSize {
return CGSize(width: CGFloat(self.numberOfColumns)*cellWidth, height: CGFloat(self.numberOfRows)*cellHeight)
}


override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {


var layoutAttributes = [UICollectionViewLayoutAttributes]()


for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}

CollectionView:

let CellIdentifier = "CellIdentifier"
let HeadCellIdentifier = "HeadCellIdentifier"


class CollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {


init() {
let layout = CustomCollectionViewLayout()


super.init(frame: CGRect.zero, collectionViewLayout: layout)


self.register(UINib(nibName: "CollectionViewCell", bundle: nil), forCellWithReuseIdentifier: CellIdentifier)
self.register(UINib(nibName: "CollectionViewHeadCell", bundle: nil), forCellWithReuseIdentifier: HeadCellIdentifier)


self.isDirectionalLockEnabled = true
self.dataSource = self
self.delegate = self
}


required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}


func updateCollectionView() {
DispatchQueue.main.async {
self.reloadData()
}
}


// MARK: CollectionView datasource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 20
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 20
}
override func numberOfItems(inSection section: Int) -> Int {
return 20
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {


let column = (indexPath as NSIndexPath).row
let row = (indexPath as NSIndexPath).section


if column == 0 {
let cell : CollectionViewHeadCell = collectionView.dequeueReusableCell(withReuseIdentifier: HeadCellIdentifier, for: indexPath) as! CollectionViewHeadCell


cell.label.text = "\(row)"


return cell
}
else if row == 0 {
let cell : CollectionViewHeadCell = collectionView.dequeueReusableCell(withReuseIdentifier: HeadCellIdentifier, for: indexPath) as! CollectionViewHeadCell


cell.label.text = "\(column)"


return cell
}
else {
let cell : CollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: CellIdentifier, for: indexPath) as! CollectionViewCell


cell.label.text = String(format: "%d", arguments: [indexPath.section*indexPath.row])


return cell
}
}


// MARK: CollectionView delegate
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {


let column = (indexPath as NSIndexPath).row
let row = (indexPath as NSIndexPath).section


print("\(column)  \(row)")
}
}

Use CollectionView from ViewController:

class ViewController: UIViewController {
let collectionView = CollectionView()


override func viewDidLoad() {
collectionView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(collectionView)
self.view.backgroundColor = UIColor.red


self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[collectionView]|", options: [], metrics: nil, views: ["collectionView": collectionView]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[collectionView]|", options: [], metrics: nil, views: ["collectionView": collectionView]))
}


override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)


collectionView.updateCollectionView()
}
}

Finally you can have fancy CollectionView!

enter image description here

Expanding on noob's answer:

func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {


let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let totalSpace = flowLayout.sectionInset.left
+ flowLayout.sectionInset.right
+ (flowLayout.minimumInteritemSpacing * CGFloat(numberOfItemsPerRow - 1))
let size = Int((collectionView.bounds.width - totalSpace) / CGFloat(numberOfItemsPerRow))
return CGSize(width: size, height: size)
}

This allows for any spacing between the cells. It assumes an Int member variable called numberOfItemsPerRow and also that all the cells are square and the same size. As noted in jhilgert00's answer we must also react to orientation changes, but now by using viewWillTransitionToSize as willRotateToInterfaceOrientation is depreciated.

If you are lazy using delegate.

extension UICollectionView {
func setItemsInRow(items: Int) {
if let layout = self.collectionViewLayout as? UICollectionViewFlowLayout {
let contentInset = self.contentInset
let itemsInRow: CGFloat = CGFloat(items);
let innerSpace = layout.minimumInteritemSpacing * (itemsInRow - 1.0)
let insetSpace = contentInset.left + contentInset.right + layout.sectionInset.left + layout.sectionInset.right
let width = floor((CGRectGetWidth(frame) - insetSpace - innerSpace) / itemsInRow);
layout.itemSize = CGSizeMake(width, width)
}
}
}

PS: Should be called after rotation too

Here is the working code for Swift 3 or above, to have a two-columns layout :

func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
    

let nbCol = 2
    

let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let totalSpace = flowLayout.sectionInset.left
+ flowLayout.sectionInset.right
+ (flowLayout.minimumInteritemSpacing * CGFloat(nbCol - 1))
let size = Int((collectionView.bounds.width - totalSpace) / CGFloat(nbCol))
return CGSize(width: size, height: size)
}

Feel free to change "nbCol" to your desired number of columns.

With Swift 5 and iOS 12.3, you can use one the 4 following implementations in order to set the number of items per row in your UICollectionView while managing insets and size changes (including rotation).


#1. Subclassing UICollectionViewFlowLayout and using UICollectionViewFlowLayout's itemSize property

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
}


}

enter image description here


#2. Using UICollectionViewFlowLayout's itemSize method

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. Using UICollectionViewDelegateFlowLayout's collectionView(_:layout:sizeForItemAt:) method

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. Subclassing UICollectionViewFlowLayout and using UICollectionViewFlowLayout's estimatedItemSize property

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
}
*/


}

enter image description here

the perfect solution is to Using UICollectionViewDelegateFlowLayout but you can easily so calculate the cell width and divided on the wanted number of columns you want

the tricky is to make the width with no fraction

(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath


{
CGFloat screenWidth = self.view.frame.size.width;
CGFloat marginWidth = (screenWidth - collectionView.frame.size.width);




CGFloat cellWith = (collectionView.frame.size.width - marginWidth )/3;
cellWith= floorf(cellWith);








CGSize retval = CGSizeMake(cellWith,cellWith);




return retval;}

Swift 3.0. Works for both horizontal and vertical scroll directions and variable spacing

Specify number of columns

let numberOfColumns: CGFloat = 3

Configure flowLayout to render specified numberOfColumns

if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
let horizontalSpacing = flowLayout.scrollDirection == .vertical ? flowLayout.minimumInteritemSpacing : flowLayout.minimumLineSpacing
let cellWidth = (collectionView.frame.width - max(0, numberOfColumns - 1)*horizontalSpacing)/numberOfColumns
flowLayout.itemSize = CGSize(width: cellWidth, height: cellWidth)
}

I just wanted to append to Imanou Petit's answer #2. To ensure margins are exact regardless of screen width, I use an iterative solver with a desired margin, and the # of columns as inputs. I also added a directional flag on where their final margins will be compared to their target.

The iterative solver is shown below and returns cellWidth and margin.

private func iterativeCellSpacing(targetMargins : CGFloat,
cellsPerRow : Int,
isMinTarget : Bool) -> (CGFloat, CGFloat)
{
var w : CGFloat = 0
var m : CGFloat = targetMargins
let cols : CGFloat = CGFloat(cellsPerRow)


let numMargins : CGFloat = cols + 1.0
let screenWidth : CGFloat = collectionView!.bounds.size.width




var delta = CGFloat.greatestFiniteMagnitude
while abs(delta) > 0.001
{
let totalMarginSpacing = numMargins * m
let totalCellSpacing = screenWidth - totalMarginSpacing


if (isMinTarget)
{
w = floor(totalCellSpacing / cols)
m = ceil((screenWidth - cols * w) / numMargins)
}
else
{
w = ceil(totalCellSpacing / cols)
m = floor((screenWidth - cols * w) / numMargins)
}


delta = screenWidth - w * CGFloat(cellsPerRow) - m * numMargins
}


return (w, m)
}

I call it as such:

fileprivate var margin: CGFloat = 20
fileprivate var cellWidth : CGFloat = 80
fileprivate let cellsPerRow = 4


override func viewDidLoad()
{
super.viewDidLoad()


(cellWidth, margin) = iterativeCellSpacing(targetMargins: margin, cellsPerRow: 4, isMinTarget: true)
...
}

I then apply the cellWidth and margin values to the flow layout as such:

extension MyCollectionController : UICollectionViewDelegateFlowLayout

{ func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: cellWidth, height: cellWidth) }

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView?.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
}


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
{
return UIEdgeInsetsMake(margin, margin, margin, margin)
}


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat
{
return margin
}


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat
{
return margin
}

}

Hope this helps. There probably is an easier way to ensure margins are exact, but this is one method. Also, this code hasn't been tested for devices that allow rotating collectionviews.

Thanks,

I made a collection layout.

To make the separator visible, Set the background color of the collection view to gray. One row per section.

Useage:

let layout = GridCollectionViewLayout()
layout.cellHeight = 50 // if not set, cellHeight = Collection.height/numberOfSections
layout.cellWidth = 50  // if not set, cellWidth = Collection.width/numberOfItems(inSection)
collectionView.collectionViewLayout = layout

Layout:

import UIKit


class GridCollectionViewLayout: UICollectionViewLayout {




var cellWidth : CGFloat = 0
var cellHeight : CGFloat = 0
var seperator: CGFloat = 1


private var cache = [UICollectionViewLayoutAttributes]()






override func prepare() {


guard let collectionView = self.collectionView else {
return
}


self.cache.removeAll()






let numberOfSections = collectionView.numberOfSections


if cellHeight <= 0
{
cellHeight = (collectionView.bounds.height - seperator*CGFloat(numberOfSections-1))/CGFloat(numberOfSections)
}


for section in 0..<collectionView.numberOfSections {


let numberOfItems = collectionView.numberOfItems(inSection: section)


let cellWidth2 : CGFloat
if cellWidth <= 0
{
cellWidth2 = (collectionView.bounds.width - seperator*CGFloat(numberOfItems-1))/CGFloat(numberOfItems)
}
else
{
cellWidth2 = cellWidth
}




for row in 0..<numberOfItems {




let indexPath = NSIndexPath(row: row, section: section)


let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
attributes.frame = CGRect(x: (cellWidth2+seperator)*CGFloat(row),
y: (cellHeight+seperator)*CGFloat(section),
width: cellWidth2,
height: cellHeight)


//row_temp.append(attributes)


self.cache.append(attributes)
}
//self.itemAttributes.append(row_temp)
}


}


override var collectionViewContentSize: CGSize {


guard let collectionView = collectionView else
{
return CGSize.zero
}


if (collectionView.numberOfSections <= 0)
{
return collectionView.bounds.size
}


let width:CGFloat
if cellWidth <= 0
{
width = collectionView.bounds.width
}
else
{
width = cellWidth*CGFloat(collectionView.numberOfItems(inSection: 0))
}


let numberOfSections = CGFloat(collectionView.numberOfSections)
var height:CGFloat = 0
height += numberOfSections * cellHeight
height += (numberOfSections - 1) * seperator






return CGSize(width: width, height: height)
}


override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {


var layoutAttributes = [UICollectionViewLayoutAttributes]()


for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}


override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}

This answer is for swift 3.0 -->

first conform to the protocol :

 UICollectionViewDelegateFlowLayout

then add these methods :

    //this method is for the size of items
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width/3
let height : CGFloat = 160.0
return CGSize(width: width, height: height)
}
//these methods are to configure the spacing between items


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0,0,0,0)
}


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}

Try This,It's working perfect for me,

Follow below steps:

1. Define values in .h file.

     #define kNoOfColumsForCollection 3
#define kNoOfRowsForCollection 4
#define kcellSpace 5
#define kCollectionViewCellWidth (self.view.frame.size.width - kcellSpace*kNoOfColumsForCollection)/kNoOfColumsForCollection
#define kCollectionViewCellHieght (self.view.frame.size.height-40- kcellSpace*kNoOfRowsForCollection)/kNoOfRowsForCollection

OR

  #define kNoOfColumsForCollection 3
#define kCollectionViewCellWidthHieght (self.view.frame.size.width - 6*kNoOfColumsForCollection)/kNoOfColumsForCollection

2.Add Code in collection View Layout data source methods as below,

 #pragma mark Collection View Layout data source methods
// collection view with autolayout


- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
return 4;
}


- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
return 1;
}


- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
return UIEdgeInsetsMake(4, 4, 4, 4);
}
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return
CGSizeMake(kCollectionViewCellWidth,kCollectionViewCellHieght);
// CGSizeMake (kCollectionViewCellWidthHieght,kCollectionViewCellWidthHieght);
}

Hope this will be help for some one.

Updated to Swift 5+ iOS 13+

Collectionview Estimate Size must be none

enter image description here

Declare margin for cell

let margin: CGFloat = 10

In viewDidLoad configure minimumInteritemSpacing, minimumLineSpacing, sectionInset

 guard let collectionView = docsColl, let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
    

flowLayout.minimumInteritemSpacing = margin
flowLayout.minimumLineSpacing = margin
flowLayout.sectionInset = UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)

UICollectionViewDataSource method sizeForItemAt

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    

let noOfCellsInRow = 2   //number of column you want
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let totalSpace = flowLayout.sectionInset.left
+ flowLayout.sectionInset.right
+ (flowLayout.minimumInteritemSpacing * CGFloat(noOfCellsInRow - 1))
    

let size = Int((collectionView.bounds.width - totalSpace) / CGFloat(noOfCellsInRow))
return CGSize(width: size, height: size)
}

First add UICollectionViewDelegateFlowLayout as protocol.

Then:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
var columnCount = 3
let width  = (view.frame.width - 20) / columnCount
return CGSize(width: width, height: width)
}