Collection View Cells are resizing unintentionally when scrolling to bottom of CollectionView - ios

I am building a collection view (gallery) of images.
The layout is 3 cells per row.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width: CGFloat = (view.frame.width / 3) - 8
let height: CGFloat = width
return CGSize(width: width, height: height)
}
Everything looks great until I scroll to the bottom of the collection view.
it goes from:
3 per row, looks good
to:
super blown up picture, larger than the screen
I also get this error message:
The behavior of the UICollectionViewFlowLayout is not defined because:
the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values.
Please check the values returned by the delegate.
The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7fa97f708c70>, and it is attached to <UICollectionView: 0x7fa981022000; frame = (0 0; 414 896); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x6000019986f0>; layer = <CALayer: 0x6000017a8820>; contentOffset: {0, 498.33333333333331}; contentSize: {414, 1250}; adjustedContentInset: {88, 0, 34, 0}; layout: <UICollectionViewFlowLayout: 0x7fa97f708c70>; dataSource: <MarsRover.GalleryCollectionVC: 0x7fa97f50b610>>.
any insight would be great, I want to turn this into infinite scrolling (api prefetching) too down the road, just fyi if that means I can ignore this.

The error you're getting is pointing at the item's width as the problem, it is saying, basically that the item(s) width and spacing cannot be more than the collection view's width. This is because your collection view has a vertical scrolling.
So to achieve a correct behavior for your collection view you must set your controller as the delegate for the collection view and also adopt the UICollectionViewDelegateFlowLayout. In your case I can see you've already implemented the collectionView(_:, layout:, sizeForItemAt: method.
In your implementation there is a clear intention to divide the collectionView's width in three equal parts. The calculation is considering a third part of the self.view.width minus eight. If I assume correctly you're intending to left an inter-item spacing of 8. If that's the case you should specify it into another method:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 8
}
That will specify the inter-item spacing to 8 points.
Continuing with the cell's width and height, you must then divide the collectionView.frame.width but before that you must subtract the inter-spacing value from this quantity, because that is the remaining space for your cells.
So your implementation would be
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// We subtract 16 because there are 2 inter item spaces like:
// [CELL] [spacing:8] [CELL] [spacing:8] [CELL]
let usableWidth = collectionView.frame.width - (2 * 8)
let cellWidth: CGFloat = usableWidth / 3
let cellHeight: CGFloat = cellWidth
return CGSize(width: cellWidth, height: cellHeight)
}
This should do the layout for you.

Since all of your items are they same size you should not be setting them in the delegate method. Instead set a static size on your flowLayout (you can drag an outlet from the storyboard) in viewDidLayoutSubviews and use the layout insets to do it so you can't possibly get the math wrong:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let remainingSpace = collectionView.bounds.width
- flowLayout.sectionInset.left
- flowLayout.sectionInset.right
- flowLayout.minimumInteritemSpacing * (Constant.numberOfItemsAcross - 1)
let dimension = remainingSpace / Constant.numberOfItemsAcross
flowLayout.itemSize = CGSize(width: dimension, height: dimension)
}

First of all, why not use bounds instead of frame?
Second, the reason this is happening is most likely because you are adjusting the layout elsewhere while the collectionView is loading cells (scrolling will make it load cells).
Are you using UIScrollViewDelegate methods to do anything with layout?

Related

UICollectionViewCell sizeForItemAt values being reverted to fitItemSize somehow?

I have set up 2 collectionView in a viewcontroller, both get their data from an endpoint and then reloadData().
One collectionView act like an header tab and have its cell size depend on its intritic size and rely on insetForSection to position/align the cell in the center of the collectionView.
Another have "sort-of" fixed size for themselves where the first cell will be almost the entire width of the collectionView and then the cells after the first one will occupy semi-half the collectionView width.
I have setted-up delegate and extension methods, however for some reason the sizeforItem that focus on the second CollectionView doesn't "stick", they get reverted. When i do :
self.statusOptionCollectionView.reloadData()
self.statusOptionCollectionView.performBatchUpdates({
self.statusOptionCollectionView.layoutIfNeeded()
}) { (complete) in
debugPrint("Batch Update complete")
}
I saw a brief frame of my desired outcome but then the collectionView suddenly undo my sizeForItem code and change the cell to something akin to "size-to-fit". (Pics: Below).
Question is how do i fix this? What is causing this? Is it because i have 2 collectionView in one viewcontroller? I've tried to invalidatingLayout in viewdidlayoutsubviews but it doesn't work. "I did use storyboard but i already delete the collectionView and re-add it, didn't fix it)
I want Something Like This (Focus one the second viewcontroller layout) :
My CollectionView Layout Code is Like This (kindTabCollectionView is the "header", with center alignment) :
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
if collectionView.isEqual(self.kindTabCollectionView){
let layout = collectionViewLayout as! UICollectionViewFlowLayout
let totalCellWidth = layout.itemSize.width * CGFloat(self.kindArray.count)
let totalSpacingWidth = CGFloat(8 * (self.kindArray.count - 1))
let leftInset = (collectionView.bounds.size.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2
let rightInset = leftInset
return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
}else{
return UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if collectionView.isEqual(self.kindTabCollectionView){
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
return flowLayout.itemSize
}else{
let height = CGFloat(40.0)
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let widthMargin = (flowLayout.sectionInset.left + flowLayout.sectionInset.left + flowLayout.minimumInteritemSpacing)
if indexPath.item == 0 && indexPath.section == 0{
let width = floor(collectionView.frame.size.width - widthMargin)
return CGSize(width: width, height: height)
}else{
let width = floor((collectionView.frame.size.width / 2) - widthMargin)
return CGSize(width: width, height: height)
}
}
}
However, the result that come out is this :
(Sorry, it was just a few frame, i tried my best to screen shot it, but it did tried to change to correct frame size, but then it just revert to the small "fitToSize" pic)
Check collectionView's "Estimated Size" attribute in the Size Inspector (Storyboard). It should be set to "None" when using an extension of UICollectionViewDelegateFlowLayout to set cell's size.
As stated in the Xcode 11 Release Notes:
Cells in a UICollectionView can now self size with Auto Layout
constrained views in the canvas. To opt into the behavior for existing
collection views, enable “Automatic” for the collection view’s
estimated size, and “Automatic” for cell’s size from the Size
inspector. If deploying before iOS 13, you can activate self sizing
collection view cells by calling performBatchUpdates(_:completion:)
during viewDidLoad(). (45617083)
So, newly created collectionViews have the attribute "Estimated Size" set as "Automatic" and the cell's size is computed considering its subview dimensions, thus ignoring the UICollectionViewDelegateFlowLayout extension methods, even though they are called.

CollectionView Constraints breaking

I have a collectionView and each cell contains a tableView inside it. The gray portion is supposed to be the collectionView.
But when I run the app, I get something like this
As you can clearly see that the table view present doesn't stick to the constraints set. I get the following errors from the console.
The behavior of the UICollectionViewFlowLayout is not defined because:
the item height must be less than the height of the UICollectionView minus the section insets top and
bottom values, minus the content insets top and bottom values.
The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7f80fd28e420>, and it
is attached to <UICollectionView: 0x7f80fc08f800; frame = (0 114; 375 664); clipsToBounds = YES;
autoresize = RM+BM; gestureRecognizers = <NSArray: 0x600000d6c4b0>; layer = <CALayer: 0x6000003beee0>;
contentOffset: {0, 0}; contentSize: {0, 0}; adjustedContentInset: {0, 0, 0, 0};
layout: <UICollectionViewFlowLayout: 0x7f80fd28e420>; dataSource: <BizTiz.EventDetailController: 0x7f80fa0b7800>>.
Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.
How do I fix this? Also when I push this collectionView onto the stack, there is a small lag. I am guessing this might be due to the previous error.
Please set layout in the UICollectionViewDelegateFlowLayout.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: <cell_width>, height: <cell_height>)
}
Realized my mistake....
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height - 114)
}
I was setting the height as view.frame.height. I had forgotten to subtract the extra height at top.

SWIFT - Collection View Cell size

I have an app that uses photos from web and put them into the collection view.
in collection view i have 3 rows of cells 1:1 size which calculates from screen width / 3.
every thing is working good but there is a thing, on for ex. iphone 6s+ the cells are all tightly get together with no spacings at all. but on iphone 5s i getting some spacing between cells, in only vertical way as on the screenshot.
iPhone 6s+ Screenshot
iPhone 5s Screenshot
there is some code:
let screenSize: CGRect = UIScreen.mainScreen().bounds
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
return CGSize(width: screenSize.width / 3, height: screenSize.width / 3)
}
i have also checked if it is an image view problem but it is not.
What can i do to remove those spacings?
Creating CollectionView and then fit cells and spacing programmatically, you can try to add minimumInteritemSpacingForSectionAtIndex and minimumLineSpacingForSectionAtIndex on your own code.
func collectionViewLaunch() {
// layout of collectionView
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
// item size
layout.itemSize = CGSizeMake(self.view.frame.width / 3, self.view.frame.size.width / 3)
// direction of scrolling
layout.scrollDirection = UICollectionViewScrollDirection.Vertical
// define frame of collectionView
let frame = CGRectMake(0, 0,
self.view.frame.size.width, self.view.frame.size.height - self.tabBarController!.tabBar.frame.size.height - self.navigationController!.navigationBar.frame.size.height)
// declare collectionView
collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.alwaysBounceVertical = true
collectionView.backgroundColor = .whiteColor()
self.view.addSubview(collectionView)
self.collectionView.hidden = false
// define cell for collection view
collectionView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
// call function to load posts
loadPosts()
}
// cell line spacing
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return 0
}
// cell inter spacing
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return 0
}
// cell numb
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return picArray.count
}
Hope help you.
Do the original sum, round it down and multiply back by the number of columns.
Adjust the frame width of the collectView to this value and then cell widths will always fit the view perfectly.
Since the iPhone 5s' width is 640, dividing it by 3 would result in 213,33333333. Since iOS doesn't like those values, it will correct the value to 213, which will create a spacing when having three of those next to each other, since 213*3 does not equal 640. On the iPhone 6s Plus, the width is 1080 which is equally dividable by 3, resulting in 360, Here, no spacing will occur.
Try finding a divider which equals a natural number for any screen size and the spaces should be gone, or you try to find another solution.
I had a similar issue with horizontal and vertical collection views.
I was using a slider to set the number of columns and then resize the UIImages in the collection accordingly to the new cell size
On top of this I cut an image of an arrow in two and placed each half at the sides of the collection view so that when it displayed it had the result of drawing a full arrow in between images... I couldn't be bothered with custom seperator inserts at the time...which is where the arrow "should" have lived.
Hence every now and then I had a pixel wide gap in the arrows depending on screensize and orientation.
It IS all in the rounding up of the division result.
You have to find a width/height that divides as best you can to fit the screen most used.
You can apply some conditional resizing of the views if the values from the sums are not integers but finding the correct value to replace it with meant having to specify every eventuality.
In the end I gave up and "lived" with the single-pixel-wide gap

Autoresizing of cells in UICollectionViewCell in accordance with the frame size

How can I make the cells in collection view auto resize so that minimum of four cells can at least fit in a single row, and if the frame size is that of iPad, then more cells could fit in row. Four cells should be the minimum number of cells in a row, please see the pictures below for further clarification:
I have a collection view which allows me to add images by using the picker view controller, at first the collection view looks like this:
I can further add images to this collection view and after adding several images, it would like:
Right now if there are four images, the fourth one goes to the next row, I want system to autoresize the cells based on the frame size so that minimum four cells are shown in one row. I'm a newbie and quite new to collection view, can someone please help me on this?
you can decide size of collectionview's cell using following method:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize{
// Return different cells size
let screenBounds = UIScreen.main.bounds
var width = screenBounds.width
let totalLeftRightInsect = 16
let minimumsSpaceBetweenCell = 5
switch (Condition for detecting device ) {
case "iPhone":
let numberOfCell = 4
let totalSpace = CGFloat(( numberOfCell * spaceBetweenCell) + totalLeftRightInsect)
width = (width - totalSpace) / CGFloat(numberOfCell)
return CGSize(width: width, height: width)
case "ipad":
let numberOfCell = 6
let totalSpace = CGFloat(( numberOfCell * spaceBetweenCell) + totalLeftRightInsect)
width = (width - totalSpace) / CGFloat(numberOfCell)
return CGSize(width: width, height: width)
default:
return CGSize(width: 0.0, height: 0.0)
}}
Answer for Swift3, Xcode 8 with horizantal spacing fixed:
The issue with all earlier answer is that cell size is given CGSizeMake(cellWidth, cellWidth) actually takes all screen leaving no room for margin lines due to which collectionView tries to adjust the row/column spacing by taking one element less in each row and unwanted extra spacing.
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let linespacing = 5 //spacing you want horizantally and vertically
let numberOfCell: CGFloat = 3 //you need to give a type as CGFloat
let cellWidth = UIScreen.mainScreen().bounds.size.width / numberOfCell
return CGSizeMake(cellWidth - linespacing, cellWidth - linespacing)
}

collectionview cell dynamic width breaks layout

When i try to give a collection view's cell a dynamic with, based on the label it will contain, something strange is happening to the spacing between the cell:
If i space the items giving a static width, the cell are being layed out as expected:
The code which gives the item a width is:
func collectionView(collectionView: UICollectionView!, layout collectionViewLayout: UICollectionViewLayout!,
sizeForItemAtIndexPath indexPath: NSIndexPath!) -> CGSize
{
let element = self.default_categories[indexPath.row] as NSString
let stringSize = element.sizeWithAttributes([NSFontAttributeName: UIFont.systemFontOfSize(18.0)])
// return CGSize(width: stringSize.width, height: self.categoriesView.frame.height)
return CGSize(width: 100, height: self.categoriesView.frame.height)
}
So, basically, i don't really know where that space is coming from, when i
try to give the cells, a dynamic width.
The sectionInset is (0, 0, 0, 0) and the minimumLineSpacing is also 0.
The issues was with "minimumInteritemSpacing" property of the layout. Setting it to 0 does remove the margins between the cells, when adding dynamic size.

Resources