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.
Related
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?
When I try to get cell's intrinsicContentSize in function collectionView(_:didSelectItemAt:), the result is (-1, -1).
I am using flow layout with auto layout config of cell's subviews and did not implement collectionView(_:layout:sizeForItemAt:). Does anyone have ideas?
Edit:
the auto layout of cell has fixed width and height. I turned on self-sizing by setting estimatedItemSize and config the collectionView as following:
self.collectionView.delegate = self
self.collectionView.dataSource = self
if let layout = self.collectionView.collectionViewLayout as?
UICollectionViewFlowLayout {
layout.estimatedItemSize = CGSize(width: 100, height: 100)
}
And I try to get the cell's intrinsicContentSize in:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
let size = cell?.intrinsicContentSize
}
which gives me (-1, -1) for size
A UICollectionViewCell has no intrinsicContentSize. Why do you expect it to have one?
If you want to know what size the cell is at this moment, just ask for its bounds.size.
If you want to know what size the cell would take on if it were sized from inside by the autolayout constraints of its subviews, call systemLayoutSizeFitting(_:).
In Xcode, I created a UICollectionView and dragged some labels to the UICollectionViewCell. The issue was that when changing devices, the cell won't adjust its size to the screen size. So I implemented the following code:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
return CGSize(width: self.collectionView.frame.width * 0.9, height:self.collectionView.frame.height *0.8)
}
After this, I noticed that the cell's size did change. However, the width and height of the labels inside the cell remained the same.
I'm not sure how to resolve this problem.
Thank you in advance for your help!!
set Constraint of your labels,
like,
for Constraint pic.
your label pic.
or you can also use UI property autoresizingMask
like,
yourLbl.autoresizingMask = [.flexibleWidth, .flexibleHeight]
I have a parent ViewController which opens a second one.
Second ViewController contains a grid view like in the image.
When I go back and enter same screen again I found layout changed to this
Here in the second image, the UIImage overlaps the label.
Here is my InterfaceBuilder Settings
I change the size and EdgeInsets using following code
func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAtIndexPath: IndexPath) -> CGSize {
let width = collectionView.frame.size.width/2 - 8*2
if let cell = collectionView.cellForItem(at: sizeForItemAtIndexPath) {
return CGSize(width: width, height: cell.frame.height)
} else {
return CGSize(width: width, height: 104)
}
}
func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, insetForSectionAtIndex : NSInteger) -> UIEdgeInsets {
return UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8);
}
Why do the first time only the layout is correct and after that the layout changes, Any Clues?
Edit1:
Here is a screen shot for 3d Debugging
Try to give aspect ratio constraint from UILabel to image. Hit and then run.
But before this make sure you did not want a static height for you imageView. If you want to the height of imageView to not be increase or decrease device wise then instead of aspect ratio just give a static height constraint to imageView. Your problem will be solve.
try to set clipToBounds to true on your UIImageView in the xib file
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.