I have a horizontal UICollectionView that's contained inside a UICollectionView header of a vertical UICollectionView. The header of the vertical UICollectionView has a dynamic height that changes due to user interaction (dragging the outer UICollectionView down expands it, scrolling up collapses it). The horizontal UICollectionView is constrained to the size of the header and will also shrink in height when the header is reduced in height. When this happens I get this error
the behavior of the UICollectionViewFlowLayout is not defined because:
2018-07-19 13:42:09.959 IDAGIO[81891:2239798] 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.
2018-07-19 13:42:09.959 IDAGIO[81891:2239798] Please check the values
return by the delegate.
because the UICollectionView shrinks in height, but the cells of it keep their old size and are therefore higher than allowed (I only get this error while scrolling up, so it's not related to any insets, the heights are fine, they just don't match exactly at that point in time).
I already tried to call collectionView.collectionViewLayout.invalidateLayout() every time I get a scroll event of the vertical collection view and then return the collection view size in
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
but the layout update then is always a bit too late, so I still get the above error. Also it looks weird as the cell height also visually is a bit behind.
My question: is there a way to dynamically respond to height changes of a horizontal UICollectionView so the cell height is updated accordingly and in time? (Maybe setup some height constraint that automatically keeps the cell height equal to the collection view height?)
I know about dynamic cell sizing, but that usually means to constrain a cell to the size of it's content. What I want to achieve is to always constrain the height of the cell (and it's content) to the collection view height.
I found an easy solution myself, without having to pass or store the scroll offset at all:
Just subclass UICollectionViewFlowLayout, override shouldInvalidateLayout and set the itemSize to the size of the new bounds.
override func shouldInvalidateLayout(forBoundsChange: CGRect) -> Bool {
if !forBoundsChange.size.equalTo(collectionView!.bounds.size) {
itemSize = forBoundsChange.size
return true
}
return false
}
In Swift 4.2
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if !newBounds.size.equalTo(collectionView!.bounds.size) {
itemSize = newBounds.size
return true
}
return false
}
UICollectionView has a property derived from UIScrollView called contentinsetadjustmentbehavior, setting that to false should fix your issue if you are setting the size of the items on the collectionView to the size of the bounds of the collectionView.
Because the scrollView can be adjusting the insets which in turns changes the collectionView.adjustedContentInset
https://developer.apple.com/documentation/uikit/uiscrollview/2902261-contentinsetadjustmentbehavior
So basically setting:
collectionView.contentInsetAdjustmentBehavior = .never
Reloading data on scroll event is one of the options but it is definitely not the best one because of all the calculations involved in reloading data again and again. You'll probably find a better solution by subclassing UICollectionViewLayout and do all the calculations on prepare(). Then you can just use delegation to pass the scroll offset to your custom layout and make the cell sizing accordingly.
My layout when on split screen is not respecting the width of the split screen.
My custom view is respecting it(the black bar at the top) but anything using autolayout is not respecting the width.
I am handling rotation using
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
Is there a helper method for handling split view? Do I handle it in layoutSubview? I would have expected the UICollectionView to handle this for us.
In viewWillTransition I use
guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}
flowLayout.invalidateLayout()
I assume by "default" layout you mean a UICollectionViewFlowLayout. It looks like the itemSize width is bigger then the width of the view. Try adding a check in collectionView:layout:sizeForItemAtIndexPath: to make sure that the width is less than or equal to the collection's width.
Yes. That issue still exist in iOS 13 beta 7.
If you implement multiple-window and split two collection view side by side. The only left window will layout correct (rotate several times could see that).
Add that in view controller can fix it:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
collectionViewLayout.invalidateLayout()
}
I have seen some questions similar to this one but none have really helped me. The last item in my collection view is always lower than the other items, as you can see in the image below:
If I increase the height of the UICollectionView then the last image alligns correctly but there is a huge gap between at the top and bottom of the UICollectionView as seen in the image below:
The sizing of the cells are controlled by this code:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let image = UIImage(data: imageArray[indexPath.row])
var size = image?.size
let height = size!.height
let width = size!.width
let imagewidth = (width / height) * 214
return CGSize(width: imagewidth, height: 214)
}
From what I understand, this should be making the height of each of the images the same, meaning it doesn't matter what the height of the original image is.
I also have another collection view in which the last item is aligned correctly and is set up in the exact same way so I'm a bit confused. What do you think the problem is? I have read that this could be a bug, but is there anyway around it? I am still learning about Swift and programming in general so if you have any other tips for me, they would be massively appreciated.
Thanks
If there is a huge gap between at the top and bottom of the UICollectionView , you can solve the problem via setting viewController's automaticallyAdjustsScrollViewInsets false.
override func viewDidLoad() {
super.viewDidLoad()
//remove the blank of top and bottom in collectionView
self.automaticallyAdjustsScrollViewInsets = false
}
I have a UICollectionView that holds a bunch of a photos.
However, if I scroll to the bottom the scrollview does not let me scroll to the bottom of the last few rows (it snaps back). I have tried override the collectionView.contentSize and just adding 1000 to the height but it doesn't fix the problem.
collectionView.contentSize = CGSizeMake(collectionView.contentSize.width, collectionView.contentSize.height + 1000)
Here is a video of the problem:
https://www.youtube.com/watch?v=fH57_pL0OjQ&list=UUIctdpq1Pzujc0u0ixMSeVw
Here is my code to create cells:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("selectPhotoCell", forIndexPath: indexPath) as B_SelectPhotoControllerViewCell
let reuseCount = ++cell.reuseCount
let asset = currentAssetAtIndex(indexPath.item)
PHImageManager.defaultManager().requestImageForAsset(asset, targetSize:_cellSize, contentMode: .AspectFit, options: nil)//the target size here can be set to CGZero for a super blurry preview
{
result, info in
if reuseCount == cell.reuseCount
{
cell.imageView.image = result
cell.imageView.frame = CGRectMake(0, 0,self._cellSize.width,self._cellSize.height)
}
}
return cell
}
private func currentAssetAtIndex(index:NSInteger)->PHAsset
{
if let fetchResult = _assetsFetchResults
{
return fetchResult[index] as PHAsset
}else
{
return _selectedAssets[index]
}
}
Update:
Because I am adding this as a child view controller, there seems to be some problems with the offsetting of the scrollview. I haven't fixed it yet but when open this view without adding it as a child view to another view controller, the scrollview is the correct size
The problem was I was adding this as a child view controller.
As a result, after doing some animations, the UICollectionView bounds were sizing to the view it was attached to. As a result its height was wrong and hence why it was getting cut off.
I just came across this question from a quick Google and felt I could add something useful.
I am running a segmentedControl with a UIView that changes to different UICollectionViews on the segment change and I couldn't get the collectionView to scroll fully down.
This may not be the solution for all, but I found that if I went to the XIB I was loading in the view and set size to freeform and decrease it by the size of a cell I had removed the problem.
suspect your CollectionView's bottomAchor was not set correctly to the parent uiview's safeAreaLayoutGuide bottomAnchor
I have a UICollectionViewController using a UICollectionViewFlowLayout where my itemSize is the size of the UICollectionView. Basically, this is a line layout of cells where each cell is fullscreen and scrolls horizontally.
In my UICollectionViewFlowLayout subclass, I have overridden prepareLayout as follows:
- (void)prepareLayout {
self.itemSize = self.collectionView.frame.size;
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.collectionView.pagingEnabled = YES;
self.minimumLineSpacing = 0.0;
self.minimumInteritemSpacing = 0.0;
self.sectionInset = UIEdgeInsetsZero;
self.footerReferenceSize = CGSizeZero;
self.headerReferenceSize = CGSizeZero;
}
The UICollectionViewController is very basic returning 10 items in one section. I've included a sample project on GitHub for more detail.
Everything appears to be set up correctly. It looks right in the simulator and on the device but, when the collection view is displayed, there is an error logged to the console:
the behavior of the UICollectionViewFlowLayout is not defined because:
the item height must be less that the height of the UICollectionView minus the section insets top and bottom values.
Note also that the collection view controller in my example is in a navigation controller and while that doesn't look particularly necessary in the example, in my real-world case I need the collection view in a navigation controller.
There is a property on UIViewController–automaticallyAdjustsScrollViewInsets–that defaults to YES. This means that when a UIViewController has a UIScrollView in its view hierarchy–which is true of a UICollectionViewController–the contentInset property of that scroll view is adjusted automatically to account for screen areas consumed by the status bar, navigation bar, and toolbar or tab bar.
The documentation for that property states:
automaticallyAdjustsScrollViewInsets
Specifies whether or not the view controller should automatically adjust its scroll view insets.
#property(nonatomic, assign) BOOL automaticallyAdjustsScrollViewInsets
Discussion
Default value is YES, which allows the view controller to adjust its scroll view insets in response to the screen areas consumed by the status bar, navigation bar, and toolbar or tab bar. Set to NO if you want to manage scroll view inset adjustments yourself, such as
when there is more than one scroll view in the view hierarchy.
The solution is to set automaticallyAdjustsScrollViewInsets to NO somewhere in your UICollectionViewController subclass, such as in viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
self.automaticallyAdjustsScrollViewInsets = NO;
}
I have put an example project on GitHub that illustrates this problem and solution. There are two branches: with_error and fixed_error. Here is a diff of the change on GitHub.
iOS 11 update: automaticallyAdjustsScrollViewInsets is deprecated in iOS 11.0.
Apple recommends using UIScrollView's contentInsetAdjustmentBehavior method instead. I set this value to .never and the error has gone. You can also set this property in Interface Builder.
collectionView.contentInsetAdjustmentBehavior = .never
This issue just occured to me on 3x screens (namely the iPhone 6 Plus.) As it turned out, the autolayout engine did not really like infinite floating point values (such as .33333333), so my solution was to floor the return height in sizeForItemAt:indexPath:.
return CGSize(width: preferredWidth, height: floor(preferredHeight))
I encountered this problem when rotating the device from portrait to landscape, back to portrait. You want to invalidate the collectionView's layout upon device rotation and before the call to super, like so:
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
// Causes collection view cells to be resized upon orientation change.
// Important that this is called *before* call to super in order to prevent error from being logged to console.
[self.collectionView.collectionViewLayout invalidateLayout];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
//...
}
This way had worked for me perfectly!.
I just subtracted the top and bottom insets from the view's height as said in that error.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width , height: view.frame.height - (view.safeAreaInsets.top + view.safeAreaInsets.bottom))
}
I hope it helps!
If you have collectionView inside scrollView just put .invalidateLayout method inside viewDidLayoutSubviews as shown below:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.collectionViewLayout.invalidateLayout()
}
I found out that .invalidateLayout method inside viewWillTransitionToSize doesn't change collection view bounds on orientation change in some cases.
A fix that worked for me.
collectionViewLayout.estimatedItemSize = .zero
or do it via IB:
Estimated Size: None
If you create the collection view in the IB, the Estimated Size property (estimatedItemSize) is set to Auto. The docs say it's .zero by default but it's not.
Like Stunner, I had the same problem when rotating from landscape (picture using full width) to portrait mode. His suggestion was the only one which really helped.
Attached the code with latest Swift for his ObjC example ... and as a bonus, my code to find the center cell of the collection view. Works quite nice ;-)
/**
-----------------------------------------------------------------------------------------------
viewWillTransition()
-----------------------------------------------------------------------------------------------
*/
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
// find cell in the center of the screen and store it in a global variable
let center = self.view.convert((self.collectionView!.center), to: self.collectionView)
// get the indexPath for the cell in the center of the current screen
if let index = collectionView!.indexPathForItem(at: center) {
// store it in a class property
self.indexPathOfCenterCell = index
}
// force recalculation of margins, borders, cell sizes etc.
self.collectionView?.collectionViewLayout.invalidateLayout()
// inform UIKit about it
super.viewWillTransition(to: size, with: coordinator)
}
ios 10: topmost view was not connected to the view outlet
In my case I had property
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
set in my flow layout. So I had to switch it to some obviously wrong constant (but with some explicit height) to suppress this warning.
UICollectionView is such a thing-in-itself and sometimes absolutely unpredictable
I had similar issue.
After load cell which is full width and some height of screen. on some condition I changed the height of cell then I was getting the same error
to fix this
I used
func updateHeightPerRatio(with image:UIImage) {
let ratio = collectionView.bounds.width / image.size.width
constHeightCollectionView .constant = ceil(image.size.height * ratio)
collectionView.reloadData()
collectionView.performBatchUpdates({
collectionView.layoutIfNeeded()
}) { (completed) in
self.collectionView.reloadData()
self.layoutIfNeeded()
}
}
Solution is reload data then perform batchupdate with that collection view re -calculate the frames . after that reload collectionview again it will apply calculated frames to cell
And now there is no log for issue now.
Hope it is helpful
I was getting the same error when I was trying to embed a UICollectionView in a UITableView. I was generating a new UICollectionView in each UITableView cell, but I did not put any constraints on the height of that UICollectionView. So, when I put a constraint on the height, that error is gone!
In my case I have to reduce bottom inset (from 20 to 0) of cell as I have reduced 20 from height of collectionview
From
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
- return UIEdgeInsets(top: 10, left: 10, bottom: 20, right: 10)
+ return UIEdgeInsets(top: 10, left: 10, bottom: 0, right: 10)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let size = CGSize(width: 350, height: collectionView.bounds.size.height - 20)
return size
}