Equal horizontal spacing UICollectionView, including insets - UICollectionViewFlowLayout? - ios

I'm trying to create equal horizontal spacing between two UICollectionViewCell and between a UICollectionViewCell an the screen border. It should look like this.
I think it is possible to use UICollectionViewFlowLayout to adjust the width of every UICollectionViewCell, so they have equal spacings regardless of the phone's size. There should always only be 3 cells on each row.

First of all you should add constants for all parameters of UICollectionView to the controller:
let itemsPerRow = 3
let spaceBetweenItems: CGFloat = 20.0
let spaceBetweenLines: CGFloat = 40.0
let itemHeight: CGFloat = 50.0
You mentioned that item's width should be adjustable. You can calculate it with this function:
var itemWidth: CGFloat {
return (self.collectionView.frame.width - CGFloat(self.itemsPerRow + 1) * self.spaceBetweenItems) / CGFloat(self.itemsPerRow)
}
Of course you need to customise layout of your UICollectionView instance:
var collectionViewLayout: UICollectionViewFlowLayout {
let result = UICollectionViewFlowLayout()
result.itemSize = CGSize(width: self.itemWidth, height: self.itemHeight)
result.minimumInteritemSpacing = self.spaceBetweenItems
result.minimumLineSpacing = self.spaceBetweenLines
result.sectionInset = UIEdgeInsets(top: 0.0, left: self.spaceBetweenItems, bottom: 0.0, right: self.spaceBetweenItems)
result.scrollDirection = .vertical
return result
}
And in the function viewDidLoad or your controller you should call setupCollectionView(). Here is a code of this function:
func setupCollectionView() {
self.collectionView.frame = self.view.frame
self.collectionView.collectionViewLayout = self.collectionViewLayout
}
You will see something like this:

Related

Number of elements in collection view in landscape mode

I have a collection view where 2 cells are being placed horizontally in portrait. In landscape mode I want 5 cells to be placed accordingly.
There is a method setItemSizeForCollectionView that defines my layout based on a device orientation.
It gets called in method configureCollectionView() which is called in viewDidLoad() and in viewWillTransition() when device gets rotated.
Views are created programmatically, so the size of the main view and its safeAreaInsets are not available in viewDidLoad.
I was thinking about subtracting those insets from the device size based on the orientation.
Any hint how can I achieve proper layout?
private func setItemSizeForCollectionView(layout: UICollectionViewLayout, with size: CGSize) {
guard let layout = layout as? UICollectionViewFlowLayout else { return }
var numberOfElementsHorizontally: CGFloat
if UIDevice.current.orientation.isPortrait {
numberOfElementsHorizontally = 2
layout.itemSize = CGSize(width: size.width / numberOfElementsHorizontally,
height: size.width / numberOfElementsHorizontally)
}
if UIDevice.current.orientation.isLandscape {
numberOfElementsHorizontally = 5
layout.itemSize = CGSize(width: size.width / numberOfElementsHorizontally,
height: size.width / numberOfElementsHorizontally)
}
}
private func configureCollectionView() {
let layout = UICollectionViewFlowLayout()
let width = UIScreen.main.bounds.width
let height = UIScreen.main.bounds.height
setItemSizeForCollectionView(layout: layout, with: CGSize(width: width, height: height))
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.scrollDirection = .vertical
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(CharacterCell.self, forCellWithReuseIdentifier: CharacterCell.reuseIdentifier)
}
This is a general purpose method for selecting how many cells per row you want in a collectionView:
func sizeForCell(itemsPerRow: Int)
{
return CGSize( width: bounds.width - (minimumInteritemSpacing * itemsPerRow) / itemsPerRow, height: 200)
}
EDIT: A bit more explanation, what we're doing here is setting the cell width to the collectionView's width, then dividing it by however many cells we want in the row, then removing some more width to leave for padding (number of items * padding amount).

2 column collectionview sizing problem in some iphones

I am using this code
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let padding: CGFloat = 50
let collectionViewSize = collectionView.frame.size.width - padding
return CGSize(width: collectionViewSize/2, height: collectionViewSize/2)
}
I am able to get a 2 column collection view on all iPhones except iPhone X and iphone XR, I don't know why
How to force 2 columns for all iPhones?
You can set layout of your collectionView by creating new layout and set it's itemSize, minimumInteritemSpacing and minimumLineSpacing and then assign new layout as collectionView.collectionViewLayout:
func setCollectionViewLayout(withPadding padding: CGFloat) {
let layout = UICollectionViewFlowLayout()
let size = (collectionView.frame.width - padding) / 2
layout.itemSize = CGSize(width: size, height: size)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView.collectionViewLayout = layout
}
and then call this method in viewDidLayoutSubviews (this is moment when frames are loaded and you can calculate with collectionView's frame)
override func viewDidLayoutSubviews() {
setCollectionViewLayout(withPadding: 50)
}
Note: I would recommend you to set leading and trailing constraints of collectionView to constant 25 instead of using padding
I suggest that you calculate width according to safeAreaLayoytGuide and, if you're using UICollectionViewFlowLayout, sectionInset. For UICollectionViewFlowLayout the following code will calculate proper width:
let sectionInset = (collectionViewLayout as! UICollectionViewFlowLayout).sectionInset
let width = collectionView.safeAreaLayoutGuide.layoutFrame.width
- sectionInset.left
- sectionInset.right
- collectionView.contentInset.left
- collectionView.contentInset.right
If you need two columns, than item width will be calculated like that:
let space: CGFloat = 10.0
let itemSize = CGSize(width: (width - space) / 2, height: 100 /*DESIRED HEIGHT*/)

How to properly size and center expanding cells in UICollectionView Swift 4.0

Note, I have scoured the internet and have not found a place to both size and centers cells that works. I tried doing it myself but I keep running to bugs I can't avoid. I am new to Swift. My code:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath:IndexPath) -> CGSize {
let cellWidth = collectionView.frame.size.width / 7.0
let cellHeight = collectionView.frame.height - 4.0
let imageSideLength = cellWidth < cellHeight ? cellWidth : cellHeight
return CGSize(width: imageSideLength, height: imageSideLength)
}
//centers the cells
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
// Make sure that the number of items is worth the computing effort.
guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout,
let dataSourceCount = photoCollectionView.dataSource?.collectionView(photoCollectionView, numberOfItemsInSection: section),
dataSourceCount > 0 else {
return .zero
}
let cellCount = CGFloat(dataSourceCount)
let itemSpacing = flowLayout.minimumInteritemSpacing
let cellWidth = flowLayout.itemSize.width + itemSpacing
let cellHeight = flowLayout.itemSize.height
var insets = flowLayout.sectionInset
// Make sure to remove the last item spacing or it will
// miscalculate the actual total width.
let totalCellWidth = (cellWidth * cellCount) - itemSpacing
let contentWidth = collectionView.frame.size.width - collectionView.contentInset.left - collectionView.contentInset.right
let contentHeight = collectionView.frame.size.height
// If the number of cells that exist take up less room than the
// collection view width, then center the content with the appropriate insets.
// Otherwise return the default layout inset.
guard totalCellWidth < contentWidth else {
return insets
}
// Calculate the right amount of padding to center the cells.
let padding = (contentWidth - totalCellWidth) / 2.0
insets.left = padding
insets.right = padding
insets.top = (contentHeight - cellHeight) / 2.0
//insets.bottom = (contentHeight - cellHeight) / 2.0
return insets
}
}
I try to use two separate functions: the first to size the cells and the second to center the cells. (Note I only want new cells to expand horizontally, with a maximum of 6 cells.) However, my calculation of cell height and width in the 2nd function does not agree with how I set it in the first function, setting off a chain of issues. Any insight on how to both size and center the cells such that I can have 1-6 cells horizontally fit on my screen centered would be great.
Your layout calls are conflicting. Try following THIS Tutorial to get the hang of it.
Otherwise a good answer for this is HERE
var flowLayout: UICollectionViewFlowLayout {
let _flowLayout = UICollectionViewFlowLayout()
// edit properties here
_flowLayout.itemSize = CGSize(width: 98, height: 134)
_flowLayout.sectionInset = UIEdgeInsetsMake(0, 5, 0, 5)
_flowLayout.scrollDirection = UICollectionViewScrollDirection.horizontal
_flowLayout.minimumInteritemSpacing = 0.0
// edit properties here
return _flowLayout
}
Set it with:
self.collectionView.collectionViewLayout = flowLayout // after initializing it another way
// or
UICollectionView(frame: .zero, collectionViewLayout: flowLayout)

How do I center align my collectionView cells?

I tried this solution here but it seems to only work for vertical layout. I'm trying to make it work for a horizontal layout. In my case, I always want 3 cells on top and 2 on bottom that is center aligned.
Example:
I think this should help you a bit (I defined insets in delegate method because collectionview would otherwise center only my 1st section and leave others untouched):
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
// handling layout
// which needs to be centered vertically and horizontally
// also:
// maximum number of items = 6
// maximum number of rows = 2
// maximum number of items in row = 3
let numberOfItems: CGFloat
let numberOfRows: CGFloat
if collectionView.numberOfItems(inSection: section) > Constants.maxNumberOfItemsInRow {
numberOfItems = CGFloat(Constants.maxNumberOfItemsInRow)
numberOfRows = CGFloat(Constants.maxNumberOfRows)
} else {
numberOfItems = CGFloat(collectionView.numberOfItems(inSection: section))
numberOfRows = CGFloat(Constants.minNumberOfRows)
}
let totalCellWidth = Constants.itemSize.width * numberOfItems
let totalSpacingWidth = Constants.minimumInteritemSpacing * numberOfItems
var leftInset = (collectionView.layer.frame.size.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2
let totalCellHeight = Constants.itemSize.height * numberOfRows
let maximumSectionHeight = (Constants.itemSize.height * CGFloat(Constants.maxNumberOfRows)) + (CGFloat(Constants.maxNumberOfRows + 1) * Constants.minimumLineSpacing)
if leftInset < 0.0 { leftInset = 0.0 }
let topInset = (maximumSectionHeight - totalCellHeight) / 2
let rightInset = leftInset
return UIEdgeInsets(top: topInset, left: leftInset, bottom: topInset, right: rightInset)
}
Also, layout is defined by class:
class CollectionViewFlowLayout: UICollectionViewFlowLayout {
// MARK: - Constants
private struct Constants {
static let minimumInteritemSpacing: CGFloat = 12.5
static let minimumLineSpacing: CGFloat = 16.5
static let itemSize = CGSize(width: 64.0, height: 90.0)
}
override func prepare() {
guard let collectionView = collectionView else { return }
/// Defining flow layout for collectionview presentation
itemSize = Constants.itemSize
headerReferenceSize = CGSize(width: collectionView.dc_width, height: 1)
scrollDirection = .vertical
minimumInteritemSpacing = Constants.minimumInteritemSpacing
minimumLineSpacing = Constants.minimumLineSpacing
super.prepare()
}
}

get the actual UICollectionView item spacing

how can I get the actual interItemSpacing for a horizontal flow layout in the UICollectionView ? the only info I can get (and set) is minimumInteritemSpacing.
I need it for the sake of the following small calculation to always display 3 equally width items in the collection view regardless to screen size (i want to replace minimumInteritemSpacing with the current value of interItemSpacing in the formula):
flowLayout.itemSize = CGSizeMake((_collectionView.frame.size.width/3.0)-(flowLayout.minimumInteritemSpacing/3.0), _collectionView.frame.size.height); I put this calculation in viewDidLayoutSubviews and it's not working precisely for some screens because their current interItemSpacing is different than the minimumInteritemSpacing
thanks in advance.
You could get the actual spacing by comparing two sequential cells
Something like
let firstIndex = IndexPath(item: 0, section: 0)
let secondIndex = IndexPath(item: 1, section: 0)
if let att1 = collectionView.layoutAttributesForItem(at: firstIndex),
let att2 = collectionView.layoutAttributesForItem(at: secondIndex) {
let actualSpace = att2.frame.minX - att1.frame.maxX
print (actualSpace)
}
For creating cells with fixing spacing try this:
private let minItemSpacing: CGFloat = 8
private let itemWidth: CGFloat = 100
private let headerHeight: CGFloat = 32
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Create our custom flow layout that evenly space out the items, and have them in the center
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: itemWidth, height: itemWidth)
layout.minimumInteritemSpacing = minItemSpacing
layout.minimumLineSpacing = minItemSpacing
layout.headerReferenceSize = CGSize(width: 0, height: headerHeight)
// Find n, where n is the number of item that can fit into the collection view
var n: CGFloat = 1
let containerWidth = collectionView.bounds.width
while true {
let nextN = n + 1
let totalWidth = (nextN*itemWidth) + (nextN-1)*minItemSpacing
if totalWidth > containerWidth {
break
} else {
n = nextN
}
}
// Calculate the section inset for left and right.
// Setting this section inset will manipulate the items such that they will all be aligned horizontally center.
let inset = max(minItemSpacing, floor( (containerWidth - (n*itemWidth) - (n-1)*minItemSpacing) / 2 ) )
layout.sectionInset = UIEdgeInsets(top: minItemSpacing, left: inset, bottom: minItemSpacing, right: inset)
collectionView.collectionViewLayout = layout
}
For more information, this excellent article:
http://samwize.com/2015/11/30/understanding-uicollection-flow-layout/

Resources