UICollectionView sizeForItemAt Called But Not Sized Correctly - swift

I would like to have the first section's cells be the full width of the collection view and the second section be half of the width. On startup, the cells don't respect the given width although the height seems to be respected. After a couple of seconds, the collection view is reloaded and most of the time it sets the cells to the correct sizes. When the phone's orientation changes to landscape, the cells are drawn in the incorrect sizes again.
I've tried using UICollectionViewDelegateFlowLayout and the sizeForItemAt delegate function to set the cell sizes but they don't follow the given sizes even if the function is being called.
Currently, I am setting section insets and then calculating the remaining available width for the cells in the collection view's bounds, afterward I divide the width by the number of cells per row.
class HomeCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let sectionInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView?.contentInsetAdjustmentBehavior = .always
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 44)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return self.sectionInsets
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return self.sectionInsets.left
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var numberOfItemsPerRow: CGFloat = 1
var height: CGFloat = 0
switch indexPath.section {
case 0:
numberOfItemsPerRow = 1
height = 97
case 1:
numberOfItemsPerRow = 2
height = 126
default:
break
}
let padding = self.sectionInsets.left * (numberOfItemsPerRow + 1)
let availableWidth = collectionView.bounds.size.width - padding
let width = availableWidth / numberOfItemsPerRow
return CGSize(width: width, height: height)
}
}
This results in the cells to be sized incorrectly as in the following image. Incorrect Example Image
The only time they are sized correctly is after the collection view is reloaded. Correct Example Image
Also, when the phone turns to landscape it displays it incorrectly. Incorrect Landscape Example Image
To conclude my issue, the first section's cells should always take 100% of the available width. If possible it would be very nice if they would have an automatic height. I've tried using UICollectionViewFlowLayout.automaticSize.height but I don't think that would work. For the second section, it needs to always take 50% of the available width.

If you're using Storyboard to create the CollectionView.
I thought I had to invalidateLayout(). It turns out there's a weird bug with iOS 13.
you have to create your own UICollectionViewFlowLayout. Call something like this in your viewDidLoad()
collectionView.collectionViewLayout = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .horizontal
return flowLayout
}()

You must invalidate layout on viewDidLayoutSubviews to re-calculate sizes for cells.
I have attached code snippet. Try this one
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.collectionViewLayout.invalidateLayout()
}

Related

UICollectionViewCell zero spacing not working

I did create a custom calendar based on UICollectionView.
One UICollectionViewCell - one date in the calendar. I want to delete spacing between cells.
CollectionView layout settings -
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (collectionView.frame.width / 7), height: collectionView.frame.width / 7)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0.0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0.0
}
In different screen sizes, unless iPhone X, we will see the spacing between cells.
How to fix it?
You probably have a rounding error. You are saying
collectionView.frame.width / 7
An iPhone 11 is 414 points wide. 414 / 7 is 59.14285714. There is no way to portray a fractional point on the screen, so now what? We round down, and now there is an extra pixel space between cells.
This is occurring because of the rounding-off of decimal places when your providing collectionView.frame.width / 7 just as matt has said in his answer. Seems you are trying to avoid this because you don't want users to see that bit of gap between the cells. Mostly you would only see this on a simulator an not on a real device due to the pixel density. You can overcome this fault either by replacing the UICollectionViewFlowLayout with a UICollectionViewCompositionalLayout which is available only from iOS 12. A simple fix would be to create a custom background view for the cell that extends the UICollectionViewCell by just 1 pixel. Here is an example of how I tried to achieve this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = .purple
let view = UIView()
view.backgroundColor = .brown
cell.addSubview(view) // Comment this line and you can see the gap
view.frame = cell.bounds
view.frame.size = CGSize(width: view.frame.size.width + 1, height: view.frame.size.height + 1)
return cell
}
This was a nice little hack I found a while back. Hope this helps you too.
I found a solution -
1. Delete from storyboard leading and trailing constraints from calendar view.
2. Set center horizontally constraint
3. Set width constraint to calendar view.
4. Create #IBOutlet calendarWidthConstraint
5. In viewDidLoad -
let layout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
let width = self.view.frame.width
let cellSize = CGFloat(width / 7).rounded()
let newWidth = cellSize * 7
layout.itemSize = CGSize(width: cellSize, height: cellSize)
calendarWidth.constant = newWidth
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView.collectionViewLayout = layout

Collection View cell off center when swiping

When I swipe through my collectionView cells they are cut off the page for some reason even tho the first cell is fine. When I get the the last cell then swipe again it bounces and then the last cell displays correctly. see video
In the video the blue is an imageView in the cell. Green, orange and purple are collectionView cells and white is the collectionView background.
first give the value of collection cell spacing 0.
add the code for collection-layout.
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath:
IndexPath) -> CGSize {
let height = UIScreen.main.bounds.height
let width = UIScreen.main.bounds.width
return CGSize(width: width, height: height) }
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout: UICollectionViewLayout, insetForSectionAt
Selection: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) }
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout:
UICollectionViewLayout,minimumInteritemSpacingForSectionAt selection:
Int) -> CGFloat{
return 0 }
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout:
UICollectionViewLayout,minimumLineSpacingForSectionAt selection: Int)
-> CGFloat{
return 0 }
if still it doesn't work then send your code... :)

Set The CollectionView As Banner

I have to Collection View in my view Controller. One is Showing the image and the other is showing the ticket details. Here is the sample image.
But I can't set the proper constraint. This is what I Do with code.
let numberOfCells = 9
let kCellHeight : CGFloat = 104
let kLineSpacing : CGFloat = 2
let kInset : CGFloat = 10
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if collectionView == clnView {
return CGSize.init(width: width, height: height)
}else {
return CGSize(width: (UIScreen.main.bounds.width - 2*kInset - kLineSpacing), height: kCellHeight)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
var size = CGFloat()
if collectionView == clnView {
size = kLineSpacing
}
return size
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
var Insect = UIEdgeInsets()
if collectionView == clnView {
Insect = UIEdgeInsets(top: kInset, left: kInset, bottom: kInset, right: kInset)
}
return Insect
}
But It's not showing properly. Look
iphone 5s
iPhone XR
Please Explain the Correct way. Thanks
Your code simply defines how items are laid out inside the collection view.
Constraints should tell you where in your parent view collection view is placed, and how wide + high.
You must learn autolayout and do it step by step in your storyboard or xib for the view. With proper Y constraint, it should work as expected. If you are putting collectionview + other things inside scrollview, follow this answer.

Weird bug returning a CGSize in UICollectionView sizeForItemAtIndexPath

I have variable widths in my UICollectionView and in my sizeForItemAtIndexPath function, I ultimately return a CGSize
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var width: CGFloat = indexPath.row == 0 ? 37 : 20 // default paddings based on spacing in cell
let font = UIFont.systemFont(ofSize: 14, weight: UIFontWeightRegular)
if indexPath.row == 0 {
width += listingsFilter.stringValue.width(withConstrainedHeight: 28, font: font)
} else {
let string = viewModel.valueRangeForButton[indexPath.row - 1]
width += string.width(withConstrainedHeight: 28, font: font)
}
print(width) // some decimal e.g. 138.1239581
let w: CGFloat = width.rounded() // rounded e.g. 13
return CGSize(width: w, height: 28)
}
If I stub the return value with a number for the width, say 128, rather than have a variable, the collection view respects UIEdgeInsets. If I return the CGSize with a variable for the width, UIEdgeInsets gets ignored for every element but the first and the last.
I feel like I found some deep CoreGraphics bug.
For the last two lines, if I change them to
let x: CGFloat = 128 // or 128.0, doesn't matter
return CGSize(width: x, height: 28)
It still doesn't work. I've tried returning a CGSize with the Int initializer. I've tried casting to Ints and back to CGFloats. Nothing seems to work.
I've narrowed down the problem to this point. It doesn't have anything to do with the width code above it (which is a string extension) or anything else.
Super weird. Anyone have experience with this?
Edit: Some images
Top one is with CGSize(width: w, height: 28) where w is a CGFloat that could equal 128 or whatever other value. Bottom one is CGSize(width: 128, height: 28)
I had similar issue few months ago. As far as I remember it was really annoying trying to fix this (as in your case) but ridiculously simple.You need to make sure that your class inherits from UICollectionViewDelegateFlowLayout. It inherits from UICollectionViewDelegate. That should make the trick. Let me know if that works for you.
As i can understand from op's question
to move content within cell you need
func collectionView(_ collectionView: UICollectionView,layout collectionViewLayout:
UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 0)
}
to move spaces between respective cells you need
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat
{
return 15
}
be sure that you confront the delegate UICollectionViewDelegateFlowLayout
Having never even tried to return a hardcoded value instead of a variable from sizeForItemAtIndexPath(), I've always assumed that UIEdgeInsets weren't even supposed to affect the spacing between the cells of a UICollectionView, but only the spaces surrounding an entire section of cells. Looks like UIEdgeInsets is not intended to affect the spacing between the cells, in which case Apple's bug is the reverse of what it seems to be -- that UIEdgeInsets is oddly affecting the spacing between cells in one outlier case, when in fact it was never intended to do so in any scenario. This red herring had you reasonably assume that there must be a reliable and programmatic way to get UIEdgeInsets to set the spacing between UICollectionView cells. I have yet to find a programmatic way to do so. When I found the following storyboard solution I stopped looking for one.
After having spent a great deal of time tweaking UICollectionViews to get them to look the way I want, I have found that the only reliable way to set the spacing between their cells is to use storyboard settings as follows:
In the storyboard, select your UICollectionView, go to the attributes inspector, find the "Min Spacing" section, and type in the "For Cells" box the value you'd like (in your case, 10). The "For Cells" value is the horizontal spacing between cells. Do the same for the "For Lines" value, which is the vertical spacing between cells.
Please check the code below,
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
collectionView?.delegate = self
collectionView?.contentInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let x: CGFloat = 50 + CGFloat(arc4random_uniform(50)) // or 128.0, doesn't matter
return CGSize(width: x, height: 28)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 0)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat
{
return 15
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 50
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: "aaa", for: indexPath)
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
}
Result is
It's not related to UIEdgeInsets, it relates to width that you're calculating in code. When you set width: 128, the width provided for your cells are bigger than what they actually need (let's say 90), hence depending on your UICollectionViewCell's design, it might feel there is an UIEdgeInsets for each cell.
To solve your problem:
If you're using IB, you can set cellSpacing and lineSpacing from IB, other-wise you have to implement UICollectionViewDelegateFlowLayout

How do I set collection view's cell size via the auto layout

Hi I'm trying to resize the cell via the auto layout.
I want display the cell by the 3 by 3.
First Cell's margin left=0
Last Cell's margin right=0
And all of the cell's space has 1pt. Like an instagram.
Should I set to Cell's Size? I want set constraint via the Autolayout.
I also trying to set cell's size using the code.
Here is my code:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSizeMake(123, 123);
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return 1;
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return 1;
}
But..I think it is not exactly calculate the cell's size.
How do I set cell's size depending on the screen's size?
I have to set space the 1pt between cell's.
Is there anyway to set only using the storyboard?
If not, How do I set by the code?
Thank you.
I don't believe you can set the size by only using the Storyboard because you can't set constraints to recurring items like collection view cells that are created on the fly at runtime.
You can easily compute the size from the information you are given. In collectionView(_:layout:sizeForItemAt:) you can access the bounds of the collectionView to compute the desired size of your cell:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// Compute the dimension of a cell for an NxN layout with space S between
// cells. Take the collection view's width, subtract (N-1)*S points for
// the spaces between the cells, and then divide by N to find the final
// dimension for the cell's width and height.
let cellsAcross: CGFloat = 3
let spaceBetweenCells: CGFloat = 1
let dim = (collectionView.bounds.width - (cellsAcross - 1) * spaceBetweenCells) / cellsAcross
return CGSize(width: dim, height: dim)
}
Make sure your class adopts the protocol UICollectionViewDelegateFlowLayout.
This then works for iPhones and iPads of all sizes.
Great answer by vacawama but I had 2 issues. I wanted the spacing that I defined in storyboard to automatically be used.
And secondly, I could not get this function to invoke and no one mentioned how? In order to invoke the collectionView's sizeForItemAt you need to extend UICollectionViewDelegateFlowLayout instead of extending UICollectionViewDelegate. I hope this saves someone else some time.
extension MyViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cellsAcross: CGFloat = 3
var widthRemainingForCellContent = collectionView.bounds.width
if let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout {
let borderSize: CGFloat = flowLayout.sectionInset.left + flowLayout.sectionInset.right
widthRemainingForCellContent -= borderSize + ((cellsAcross - 1) * flowLayout.minimumInteritemSpacing)
}
let cellWidth = widthRemainingForCellContent / cellsAcross
return CGSize(width: cellWidth, height: cellWidth)
}
}
I set it up using CollectionView's delegate methods. This will give you a 2xN setup but you can easily make it a 3xN instead. Here's a screenshot and you can refer to my project on GitHub...
https://github.com/WadeSellers/GoInstaPro
Swift 3 version code based on vacawama answer:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cellsAcross: CGFloat = 3
let spaceBetweenCells: CGFloat = 1
let dim = (collectionView.bounds.width - (cellsAcross - 1) * spaceBetweenCells) / cellsAcross
return CGSize(width: dim, height: dim)
}
Here is another solution but It only works on iPhone6.
I will trying to update for iphone5/6plus
Special thanks to #vacawama!
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
// Compute the dimension of a cell for an NxN layout with space S between
// cells. Take the collection view's width, subtract (N-1)*S points for
// the spaces between the cells, and then divide by N to find the final
// dimension for the cell's width and height.
let cellsAcross: CGFloat = 3
let spaceBetweenCells: CGFloat = 1
let dim = (collectionView.bounds.width - (cellsAcross - 1) * spaceBetweenCells) / cellsAcross
print(indexPath.row)
var width = dim
//last cell's width
if( (indexPath.row + 1) % 3 == 0){
width = 124
}else {
width = 124.5
}
print(width)
return CGSize(width: width , height: dim)
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return 1;
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return 1;
}