UICollectionView 3 column grid, 1px space?? (image included) - ios

I have setup a UICollectionView with a FlowLayout that leaves no space in between cells in either the vertical or horizontal directions.
I have everything working, yet there is an odd 1px space between the 2nd and 3rd column and I have no idea why!? I have verified the 1px gap shows up both in iOS simulator and on a real device. Has anybody experienced this?
My UIViewController is a delegate/datasource of the following:
class MyViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
I have implemented the necessary functions to remove any spacing (and verified with print statements that they are running), as well as visual confirmation of the cells lining up next to each other:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return UIEdgeInsetsZero
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return 0
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return 0
}
Finally, I have printed out the width of the cell being returned (each color square) to verify it was the (view.frame.width / 3) or (320/3), which is 106.666667
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
var totalHeight: CGFloat = (self.view.frame.width / 3)
var totalWidth: CGFloat = (self.view.frame.width / 3)
println(totalWidth) // this prints 106.666666667
return CGSizeMake(totalWidth, totalHeight)
}

Better avoid number's double type in similar situations.
Use:
return CGSizeMake(ceil(totalWidth), ceil(totalHeight))

I was having the same problem, I fixed it with the code below:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let paddingSpace = sectionInsets.left * 4
let availableWidth = view.frame.width - paddingspace
let widthPerItem = availableWidth/3
return CGSizeMake(width: widthPerItem, height: widthPerItem)
}
and define the variable below in the begin of your class:
fileprivate let sectionInsets = UIEdgeInsets(top: 0.0, left: 0.5, bottom: 0.0, right: 0.0)

For example you have screen width = 320, when you divide it by 3 you have 106.6666. If consider it as Int, we have: 106 * 3 = 318. We need 2 pixel to be added by 1 px to first two cells because 320 - 318 = 2. Try this solution below.
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let columns = 3
let width = Int(UIScreen.main.bounds.width)
let side = width / columns
let rem = width % columns
let addOne = indexPath.row % columns < rem
let ceilWidth = addOne ? side + 1 : side
return CGSize(width: ceilWidth, height: side)
}

Here is my solution to issue just make middle cell of width 106 and other two with width 107 in method collectionView layout
NSInteger numColumns = 3;
CGSize deviceSize;
CGFloat width = self.view.frame.size.width / numColumns;
CGFloat height = width ;
NSInteger rem = (int)self.view.frame.size.width%numColumns;
if(indexPath.row == (1 + numColumns*counter) && rem != 0) {
deviceSize = CGSizeMake(floor (width), ceil(height));
counter++;
}
else {
deviceSize = CGSizeMake(ceil (width), ceil(height));
}
deviceSize = CGSizeMake(deviceSize.width, deviceSize.height);

Related

Collection view grid last row appears differently despite equal widths - swift iOS

I've a fixed grid of collection view cells (UICollectionView) but the cells in the bottom row always appears with a slightly smaller width on screen. The frame size (or bounds) and calculated width used within collectionViewLayout: UICollectionViewLayout sizeForItemAt are the same for all rows.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let settings = currentContents[indexPath.item]
let height = CGFloat(30)
// https://stackoverflow.com/questions/54915227/uicollectionview-remove-space-between-cells-with-7-items-per-row
var cellWidth = CGFloat()
let availableWidth = collectionView.bounds.size.width
print("available width", availableWidth)
let minimumWidth = floor(availableWidth / collectionContents.cellsPerRow5)
print("minmum width", minimumWidth)
cellWidth = minimumWidth * settings.portion - 1
print("cell width", cellWidth)
return CGSize(width: cellWidth, height: height)
}
I'd like to get the bottom row to line up with the other rows, but can't imagine what is happening that is changing the widths after returning the value in the layout delegate method (or how to fix).
UIKit does not like values with more than 2 decimals, round them or it will do it for you.
Here, UICollectionViewFlowLayout rounds your cell size and starts to fill the lines with an "interitemspacing" at least equal to the minimumInteritemSpacing you specified. On the last line, it used exactly the minimumInteritemSpacing value and doesn't fill entirely the line.
Fix it using better rounded values, giving the illusion that all is perfectly aligned.
I usually use those extensions:
extension CGFloat {
func xx_rounded(_ rule: FloatingPointRoundingRule = .down, toDecimals decimals: Int = 2) -> CGFloat {
let multiplier = CGFloat(pow(10.0, CGFloat(decimals)))
return (self * multiplier).rounded(.down) / multiplier
}
}
extension CGSize {
func xx_rounded(rule: FloatingPointRoundingRule = .down, toDecimals: Int = 2) -> CGSize {
return CGSize(
width: width.xx_rounded(rule, toDecimals: toDecimals),
height: height.xx_rounded(rule, toDecimals: toDecimals)
)
}
}
Change:
return CGSize(width: cellWidth, height: height)
to
return CGSize(width: cellWidth, height: height).xx_rounded()
Just replace
cellWidth = (minimumWidth * settings.portion) - 0.5
WITH
cellWidth = (minimumWidth * settings.portion)
Specified minimumInteritemSpacingForSectionAt to 0.5 instead of 0
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0.5
}
CVTVCell.swift
Just reload your UICollectionView in awakeFromNib method after your data set into currentContents. Please refer below sample code.
override func awakeFromNib(){
super.awakeFromNib()
currentContents = collectionContents.allEndings[0]
self.cView.reloadData()
}
Download code from here
The best way I found was to capture the remaining space (minus 1 to avoid rounding up) after calculating the initial column width, and then use this remaining space for additional columns.
For a two column example:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
if indexPath.item == 0 || indexPath.item == 1
{ rowHeight = 40 }
else { rowHeight = 30 }
var cellWidth = CGFloat()
let minimumWidth = floor(availableWidth / numberOfColumns)
var columnPortion = CGFloat()
let columnNumber = indexPath.item % Int(numberOfColumns)
switch columnNumber
{
case 0:
columnPortion = columnPortion1
cellWidth = minimumWidth * columnPortion - 10
remainder = availableWidth - cellWidth - 1
case 1:
columnPortion = columnPortionFinal
cellWidth = remainder
default:
break
}
return CGSize(width: cellWidth, height: rowHeight)
}

how to use center vertically cells in collection view in paging mode in swift 4?

I read
How to vertically center UICollectionView content
But when I used the codes here
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let navBarHeight = self.navigationController?.navigationBar.frame.height
let collectionViewHeight = (self.collectionView?.frame.height)! - navBarHeight!
let itemsHeight = self.collectionView?.contentSize.height
let topInset = ( collectionViewHeight - itemsHeight! ) / 4
return UIEdgeInsetsMake(topInset ,0, 0 , 0)
}
But when scrolling collection view this will show incorrect form
so here is my codes because my collection view cells are square and equal to screen width
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
showImagesCV.contentInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
if let flowLayout = showImagesCV.collectionViewLayout as? UICollectionViewFlowLayout{
cellWidth = UIScreen.main.bounds.width
cellHeight = cellWidth //yourCellHeight = cellWidth if u want square cell
flowLayout.minimumLineSpacing = spacing
flowLayout.minimumInteritemSpacing = spacing
UIView.performWithoutAnimation {
}
}
}
}
extension showImagesViewController : UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: cellWidth, height: cellHeight)
}
}
I want the spacing between cells be 0 and each cell that is showing (current page (because the collection view is in paging mode)) be the center of the screen
Regarding the SO Question you mentioned and using, that is to centre the whole section not individual cells. Some of your code might be invalid(needs to be redone).
First and most important, set the constraints of your collectionView for full screen. Remember to take safeAre/TopGuide into account, this will make sure that collection view is below the navigation bar if there is one.
This will make sure that your collectionView is up to date
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myCollectionView.collectionViewLayout.invalidateLayout()
}
Dump/comment below code and set insets to (0,0,0,0) in inspector of collectionView in story board. In same inspector change Min Spacing to 0 for cells and for line, both.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let navBarHeight = self.navigationController?.navigationBar.frame.height
let collectionViewHeight = (self.collectionView?.frame.height)! - navBarHeight!
let itemsHeight = self.collectionView?.contentSize.height
let topInset = ( collectionViewHeight - itemsHeight! ) / 4
return UIEdgeInsetsMake(topInset ,0, 0 , 0)
}
Also remove/comment below code
showImagesCV.contentInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
if let flowLayout = showImagesCV.collectionViewLayout as? UICollectionViewFlowLayout{
cellWidth = UIScreen.main.bounds.width
cellHeight = cellWidth //yourCellHeight = cellWidth if u want square cell
flowLayout.minimumLineSpacing = spacing
flowLayout.minimumInteritemSpacing = spacing
UIView.performWithoutAnimation {
}
}
Now change your sizeForItemAt like this.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return collectionView.frame.size
}
UICollectionViewCell
Now in your collectionView cell, make sure that your image view is 1:1 and in centre. Also handle constraints of your other UI components.
PS : This is easy approach, other would be to write custom flowLayout

How to autolayout ImageView inside CollectionView so that cell to edge has same spacing as inner cells spacing?

I created UIImageView inside CollectionViewCell. I wanted for the layout to have the same spacing between the left most cell to left edge as spacing between inner cell, and same spacing for right most cell to the right edge of the screen. The current layout I have is like this:
In the image above the left edge to left most cell has 10 points spacing while the inner cells spacing is twice of that. That is because I have the UIImageView with 10points smaller in all 4 directions (up,down,left,right) against the CollectionViewCell.
I have spend significant time on this issue including searching StackOverflow but can't seems to find the answer.
I have no issue to create N number of cells in a row as per this posting and I've also played with the cell spacing as per this posting
I managed to find the solution to the above problem.
I did the following:
Remove all inner spacing between UIImageView and the CollectionViewCell. Then remove any offset and make the size match. Put constraint of 0 on all 4 directions.
Use the second reference mentioned above as template. I updated my ViewController with following contents:
extension ToDoCategoryViewController: UICollectionViewDelegateFlowLayout {
fileprivate var sectionInsets: UIEdgeInsets {
return UIEdgeInsetsMake(0, Constant.hardCodedPadding, 0, Constant.hardCodedPadding)
}
fileprivate var innerSpacing: CGFloat { return Constant.hardCodedPadding }
fileprivate var rowSpacing: CGFloat { return Constant.hardCodedPadding }
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var itemPerRow: CGFloat {
switch collectionView.bounds.width {
case 300..<500:
return 3
case 500..<800:
return 4
default:
return 5
}
}
let innerSpacingCount = itemPerRow - 1
let edgesPadding = Constant.hardCodedPadding * 2.0
let itemWidth = (collectionView.bounds.width - (innerSpacing * innerSpacingCount + edgesPadding)) / itemPerRow
let itemHeight = itemWidth * CGFloat(1.25)
return CGSize(width: itemWidth, height: itemHeight)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return sectionInsets
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return rowSpacing
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return innerSpacing
}
}
In my code elsewhere, I set the Constant.hardcodedPadding value, e.g. CGFloat(30.0)
Thus, I get the following view:

UICollectionViewDelegateFlowLayout Edge Insets not getting recognized by sizeForItemAt function?

So as the title suggest, I seem to have run into a weird problem. All I am trying to do here is create a 2 column collectionview without hardcoding anything into my delegate methods. Upon debugging I found that insetForSectionAt is called after sizeForItemAt, hence the custom insets are not taken into consideration when calculating the size of every cell.
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
print("left: ", flowLayout.sectionInset.left) // Prints out 0.0
print("right: ", flowLayout.sectionInset.right) // Prints out 0.0
let marginsAndInsets = flowLayout.sectionInset.left + flowLayout.sectionInset.right + flowLayout.minimumInteritemSpacing * (cellsPerRow - 1)
let itemWidth = (collectionView.bounds.size.width - marginsAndInsets) / cellsPerRow
return CGSize(width: itemWidth, height: itemWidth)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
}
}
This problem can be solved very uglily by hard coding the values directly into the itemWidth variable as such, -20 because i know the values of left and right insets, 10 + 10
let itemWidth = (collectionView.bounds.size.width - marginsAndInsets - 20) / cellsPerRow
however, I have to believe that there is a better way of doing this, how can I call reloadData upon completion of the UIEdgeInset calculation so that my cells are properly sized?
So this is what I ended up doing, i just added the customizations directly when creating the sizes for the collectionView and its cells.
class PinController: UICollectionViewController {
fileprivate let pinCellId = "pinCellId"
fileprivate let cellsPerRow: CGFloat = 2.0
fileprivate let margin: CGFloat = 10.0
fileprivate let topMargin: CGFloat = 2.0
}
extension PinController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let flowLayout = customize(collectionViewLayout, margin: margin)
let itemWidth = cellWidth(collectionView, layout: flowLayout, cellsPerRow: cellsPerRow)
return CGSize(width: itemWidth, height: itemWidth * 1.1)
}
/*
Customizes the layout of a collectionView with space to the left and right of each cell that is
specified with the parameter margin
*/
private func customize(_ collectionViewLayout: UICollectionViewLayout, margin: CGFloat) -> UICollectionViewFlowLayout {
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
// Interitem spacing affects the horrizontal spacing between cells
flowLayout.minimumInteritemSpacing = margin
// Line spacing affects the vertical space between cells
flowLayout.minimumLineSpacing = margin
// Section insets affect the entire container for the collectionView cells
flowLayout.sectionInset = UIEdgeInsets(top: topMargin, left: margin, bottom: 0, right: margin)
return flowLayout
}
/*
Calculates the proper size of an individual cell with the specified number of cells in a row desired,
along with the layout of the collectionView
*/
private func cellWidth(_ collectionView: UICollectionView, layout flowLayout: UICollectionViewFlowLayout, cellsPerRow: CGFloat) -> CGFloat {
// Calculate the ammount of "horizontal space" that will be needed in a row
let marginsAndInsets = flowLayout.sectionInset.left + flowLayout.sectionInset.right + flowLayout.minimumInteritemSpacing * (cellsPerRow - 1)
let itemWidth = (collectionView.bounds.size.width - marginsAndInsets) / cellsPerRow
return itemWidth
}
}

Collectionview Fixed Cell Spacing

I've been looking for how to make UICollectionView have a fixed spacing between its cells, but have not found a solution so far.
I have a UICollectionView which consists of two columns, and all the cells have the same width, but they vary in height. I have only one section in this UICollectionView.
I have set the minimum spacing to 10(which is the value I want between cells vertically), but the system is seemingly randomly stretching the vertical space.
What I would like to achieve, is the effect Wikipedia for iPad has: different article tiles with the same width, but adjustable height according to need, yet all the cells are spaces evenly vertically. (I don't mind leaving space at the very end, but I don't want extra spaces in the middle)
How should I achieve this? I would accept third-party libraries if it is easy to migrate existing UICollectionViews(and it's open-source, of course).
In swift 3,
you can implement this delegate method for cell spacing in collectionView :
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize{
// Give cell width and height
return CGSize(width: 750, height: 320)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat{
// splace between two cell horizonatally
return 50
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat{
// splace between two cell vertically
return 100
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets{
// give space top left bottom and right for cells
return UIEdgeInsets(top: 0, left: 100, bottom: 100, right: 100)
}
All right, so the reason after all is that the spacing that I've set was the MINIMUM spacing between cells. The collectionView could use whatever spacing needed to align cells on the same row, as long as it's not less than the minimum.
If you want to have fixed inter item spacing on UICollectionView you can use the custom layout below. Note: I've created this layout for fixed height cells and .vertical scroll direction.
class CollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let layoutAttributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
var startX: CGFloat = 0.0
var startY: CGFloat = 0.0
let contentWidth = collectionView?.frame.width ?? 0
for layoutAttributes in layoutAttributes {
let size = layoutAttributes.size
if (startX + size.width + minimumInteritemSpacing) > contentWidth {
startX = 0
startY += size.height
startY += minimumLineSpacing
}
let origin = CGPoint(x: startX, y: startY)
let frame = CGRect(origin: origin, size: size)
layoutAttributes.frame = frame
startX += size.width + minimumInteritemSpacing
}
return layoutAttributes
}
}

Resources