I have a set of 4 different elements that can be shown in a small horizontal collectionView. There will only be one row, and the collectionView and the elements will always be the same height as each other - however, it needs to be dynamic, in case people increase their text size (accessibility larger text).
To make this work, I have a "spacer" label next to the CollectionView, and tell the collectionView to be the same height as the label, and I do the same in all the cells. They all have the same font.
When I try to run it, the CollectionView prints out this:
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.
Please check the values returned by the delegate.
The relevant UICollectionViewFlowLayout instance is <...>,
and it is attached to <..; frame = (0 438.5; 414 29.5);
clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <..>;
layer = <CALayer:..>; contentOffset: {0, 0}; contentSize: {32, 29.5};
adjustedContentInset: {0, 0, 0, 0}> collection view layout: <...>.
Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes
to catch this in the debugger.
But everything I can find returns 29.5 or less.
collectionView.contentInsetAdjustmentBehavior = .never
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
ContentInset is .zero, sectionInset is 0 for top and bottom, lineSpacing and interItem-spacing is 0, scrollDirection = .horizontal..
I have not overridden any sizeForItem or anything like that, because it should happen automatically, and I shouldn't have to create custom height logic.
I have tried this in each cell:
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
setNeedsLayout()
layoutIfNeeded()
layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(UICollectionViewCell.layoutFittingCompressedSize)
print("Height: ", layoutAttributes.size)
return layoutAttributes
}
But it just prints out 29.5, which should be correct.
It does print it out after the warnings though, so something happens before this.
When I print out the height of the cell directly after its initialization like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let item = items[indexPath.row]
if let someItem = item as? SomeCellViewModel, let cell = self.dequeueReusableCell(withReuseIdentifier: "SomeIdentifier", for: indexPath) as? SomeCell{
cell.bind(with: someItem)
print("height: ", cell.bounds.size.height)
return cell
}else{ ...
it prints out "50.0". And as I understand, that's the default "Item Size", but I've changed all that! In Storyboard/IB/Xib, the "Collection View Flow Layout" was 50x50, but I have changed it to different things (0x0, 1x1, 20x20, tested a lot). Still it prints out 50.0. The xibs for the cells aren't even 50 in their file. If I try to use the systemLayoutSize-thing directly here to set its frame.size, it warns me about an autoresizing constraint that wants to be 0. I assume I shouldn't be doing such logic here anyway, so I nevermind this.
Since the warning says items "must be less than the height minus X minus Y" (not less than or equal to), I tried making the CollectionView a point taller than the spacer-logic, making the CollectionView 30.5 and the cells 29.5, but I still get the warning.
I have tried setting the symbolic breakpoint, but there is nothing of value there..
The thing is - it looks and behaves correct - on iOS 12. It does not work on iOS 11. There might be a separat constraint-issue within the elements that makes it wrong on iOS 11, I'm looking into it. Either way;
Why am I receiving this warning?
Edit: if I implement sizeForItemAt:IndexPath and statically tell it to be
width:100, height: 29.5, there is no warning at all. Everything is good. But I don't want to do that. I shouldn't need to calculate the individual height like this. And I don't really want to deal with prototype-cells either.
This should work.
Related
I'm trying to learn how to deal with UICollectionViews in a Swift iOS app - just a simple app that displays an image and a label in the cell 25 times. Initially bad constraints were preventing the CollectionView from displaying anything, but once I worked those out, the CollectionView appears to display cells as desired and expected. Debug messages were displayed from my Layout method for all 25 items as expected.
Then I scrolled down once it got passed the 11th item, it seems to completely lose its layout as you can see in the following image. Scrolling upward, it's even more wonky, all the cells turn into a full size image.
Example in simulator
There's nothing special about the code, especially the layout - here's that code:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.item > 10 {
// Limit number of debug messages
print("view.bounds: \(view.bounds)")
print("collectionView.bounds: \(collectionView.bounds)")
print("collectionView.alignmentRectInsets: \(collectionView.alignmentRectInsets)")
print("collectionView.contentInset: \(collectionView.contentInset)")
}
let width = view.bounds.width
print("width[\(indexPath.item)]: \(width)")
let cellDimension = (width / 2 ) - 15
print("cellDimension[\(indexPath.item)]: \(cellDimension)")
let returnSize = CGSize(width: cellDimension, height: cellDimension)
print("returnSize[\(indexPath.item)]: \(returnSize)")
return returnSize
}
Once this goes past the 11th item, the debugger starts spewing lots and lots of messages:
2019-10-22 01:32:54.794032-0500 CollectionTest#1[13173:339022] The behavior of the UICollectionViewFlowLayout is not defined because:
2019-10-22 01:32:54.794203-0500 CollectionTest#1[13173:339022] 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.
2019-10-22 01:32:54.794360-0500 CollectionTest#1[13173:339022] Please check the values returned by the delegate.
2019-10-22 01:32:54.794856-0500 CollectionTest#1[13173:339022] The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7fe4c1407750>, and it is attached to <UICollectionView: 0x7fe492819400; frame = (0 0; 414 736); clipsToBounds = YES; autoresize = LM+W+RM+TM+H+BM; gestureRecognizers = <NSArray: 0x600000965050>; layer = <CALayer: 0x60000076efe0>; contentOffset: {0, 355}; contentSize: {414, 2512}; adjustedContentInset: {20, 0, 0, 0}> collection view layout: <UICollectionViewFlowLayout: 0x7fe4c1407750>.
2019-10-22 01:32:54.801289-0500 CollectionTest#1[13173:339022] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.
This just goes on and on ... abbreviated for brevity.
Thing is, the cells were all OK before.
Additional code:
CollectionViewCell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var cellImage: UIImageView!
#IBOutlet weak var cellLabel: UILabel!
}
ViewController.swift
import UIKit
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 25
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
cell.cellImage.image = UIImage(named: "file-35")
cell.cellLabel.text = "cell# \(indexPath.item)"
cell.layer.borderWidth = 0.5
cell.layer.borderColor = UIColor.lightGray.cgColor
cell.layer.cornerRadius = 10
cell.backgroundColor = UIColor.black
cell.cellImage.layer.cornerRadius = 10
print("indexPath.item: \(indexPath.item)")
print("view.bounds: \(view.bounds)")
print("collectionView.bounds: \(collectionView.bounds)")
print("collectionView.contentInset: \(collectionView.contentInset)")
print("collectionView.alignmentRectInsets: \(collectionView.alignmentRectInsets)")
print("layoutAttributesForItem: \(String(describing: collectionView.layoutAttributesForItem(at: indexPath)))")
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
collectionView.delegate = self
collectionView.dataSource = self
// view.translatesAutoresizingMaskIntoConstraints = true
}
By the way, it doesn't seem to make any difference which version of the runtime simulator is used.
Any help, pointers, suggestions, etc., is greatly appreciated while I still have some hair left to pull out. Thank you.
Try setting the clipToBounds property of the cellImage inside CollectionViewCell to true, i.e.
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var cellImage: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
self.cellImage.clipsToBounds = true //here..
}
//rest of the code....
}
You can also set the clipToBounds of cellImage in the UI itself, i.e.
Note: Move all the code for cell layout in the CollectionViewCell instead of writing everything in cellForItemAt. That'll make the code less bulky and readable.
Thanks for the suggestions. I found a Stack Overflow article that pushed me in a direction that seems to have helped solve the problems.
I could be wrong, but my observations tell me that it seems that not all Cells get their Size initialized from the Storyboard values. The first dozen or so do but when the collection view starts reusing / recycling cells, it loses track of the cell size. I tried setting the cell size in cellForItemAt() but that causes some really bad recursive problems.
Watching with the debugger, sizeForItemAt() only gets called initially, not when cells start being reused, and when the cell size has been lost, all sorts of undesirable things begin to happen.
Initializing the cell size in viewDidLoad() eliminated most of the problems, that is until the device is rotated, then sizeForItemAt() gets called in iPhone simulators for the new layout, but not in iPad simulators.
The solution to this is to check for the device type in viewDidLoad() and compute the desired number of cells per row. Simply setting the cell size of the layout doesn't completely solve the problem if you want a different number of cells per row when the device is rotated.
This seems to have tamed the problems, for now at least.
I'm just a bit concerned that I'm missing something because the sizeForItemAt() function doesn't get called as expected.
I'm having this weird issue with UITableView that can't calculate it's content's height properly.
I have custom UITableView class that is embedded in another custom UITableView, I want it to auto-adjust it's height to fit content so I have already:
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
And now when I use:
self.estimatedRowHeight = UITableView.automaticDimension // non-zero value like 40 isn't working either
self.rowHeight = UITableView.automaticDimension
the output is the frame that is not full height, when I turn "Scrolling enabled" in this TableView it's scrollable with full content (don't want that):
Now when I change
self.estimatedRowHeight = UITableView.automaticDimension
to:
self.estimatedRowHeight = 0
the output is exactly what I would want to have except the content text is cut...
Here's my CommentCell:
Console isn't showing any errors with autolayout in any case.
Do you maybe know what's going on? I have spent literally days trying to get those comments to work and that's the last thing I need.
If you need any more info please just tell me.
Edit:
If i change estimatedRowHeight to a large number for example 500 I get loads of empty space under cells:
So it looks like TableView can't fix the cell height to content. Maybe this will help someone.
Maybe it's about the textfield inside the CellView. Did you set it's Layout to wraps?
Also I would try to set it's intrinsic size value to 'placeholder' inside the Size Inspector.
I understand that this question has been asked here, but it didn't solve my problem as the link in the accepted answer is down and the minimal example didn't help.
Here is a picture of my custom UITableViewCell & all its constraints:
Each post contains these UI elements. The only element that could make each cell's height different is messageView, because its height depends on the string being displayed. Question is, how do I dynamically set each cell's height? Here's what I have now (Does NOT work, messageView is not shown at all):
func cellForRowAt(indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(...) as! PostCell
let message = ...
cell.messageView.text = message
return cell
}
func heightForRowAt(indexPath: IndexPath) -> CGFloat {
var cellMinimumHeight: CGFloat = 120
if let cell = tableView.cellForRow(at: indexPath) as? PostCell {
let size = cell.messageView.sizeThatFits(cell.messageView.frame.size)
cellMinimumHeight += size.height
}
return cellMinimumHeight
}
in heightForRowAt function, the if is not being executed, therefore, all cells' heights are cellMinimumHeight = 120.
How could I make each cell's height = 120 + messageView's height?
---------------------------------EDIT 1---------------------------------
Made a mistake in the picture, messageView's height is not set
When using Auto-Layout for dynamically sizing cells, you don't really need to implement sizeThatFits(...). If the constraints are setup correctly, then you only need to disable the scrolling of the UITextView.
From code:
yourTextView.scrollEnabled = false
From IB:
Select your Text View and open Attributes inspector, then
In Attributes Inspector select your UITextView(messageView) and Uncheck "Scrolling Enabled".
And then change your UITextView(messageView)'s content compression resistence priority as follows:
Horizontal = 750
Vertical = 1000
I hope this will help you.
Just disable UITextview scroll...
But here is no use of UITextview, you can use label also.
In HeightForRow tableview delegate method remove that stuff and use
return UITableViewAutomaticDimension
You have used constrain to make cell hight dynamic
form apple documentation
To define the cell’s height, you need an unbroken chain of constraints
and views (with defined heights) to fill the area between the content
view’s top edge and its bottom edge.
So for your case you need
cell's height = 120 + messageView's height?
So start from Profile Image to measure unbroken chain of constraints from Top to Bottom
Profile Image top = 10 + ImageHeight = 60 ----> 70
MessageView top = 10 + set minimum height to message say 20 one line if every cell should have message even if one word and set this height Greater than or equal to 20 make sure that you set Scroll enable = false
so message Height minimum = 10 top + 20 + 10 bottom ---> 40
Menu Stack view Height ---> 30
So all Total = 70 + 40 + 30 = 140 this default hight no cell will be less than this
Also you must set the table view’s rowHeight property to UITableViewAutomaticDimension. You must also assign a value to the estimatedRowHeight property
tableView.estimatedRowHeight = 130.0
tableView.rowHeight = UITableViewAutomaticDimension
Here is apple documentation Here
So I have a collection view that uses a custom layout that I found on Github called CHTCollectionViewWaterfallLayout I like it so far, however I am looking to make the custom collection view cell I made dynamic in height, and I am having trouble connecting Auto Layout to calculate this. I shared a simple sample project on Github that generates random string sizes and displays them, only problem is that Auto Layout generates the same cell height for each Collection View Cell. The project can be found here.
To give you a run down of my thought process, I calculate the cell height by using the method CHTCollectionViewDelegateWaterfallLayout sizeForItemAtIndexPath. Since I also use the delegates method columnCountforSection my thought is since I provide a finite number of columns based on the orientation, I take the collectionView frame width and I divide by the number of columns to get me my width for the cell.
func collectionView (collectionView: UICollectionView,layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
var cell = dict["heightCell"] as? CollectionViewCell
let randomString = RadomStrings[indexPath.item]
let float = CGFloat(columnCount)
let width = collectionView.bounds.size.width / float
if cell == nil {
cell = NSBundle.mainBundle().loadNibNamed("CollectionViewCell", owner: self, options: nil)[0] as? CollectionViewCell
dict["heightCell"] = cell
}
cell?.RandomStringLabel.text = randomString
cell!.frame = CGRectMake(0, 0, CGRectGetWidth(collectionView.bounds), CGRectGetHeight(cell!.frame))
cell!.setNeedsLayout()
cell!.layoutIfNeeded()
var size = cell?.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
size?.width = width
return CGSize(width: width, height: size!.height)
}
My constraints are pretty basic. I have a constraint on each axis. One on the top of the content view, leading, trailing and bottom. I also have a height on the label that is greater than or equal to 45.
Using Auto Layout to calculate TableViewCell heights is easy to me and I like it because I like the DRY principle behind this height calculation approach. So I would like to keep this height calculation process the same throughout my app. CollectionViews are a relatively new layout process for me, so I would love to learn what I am doing wrong here. Hopefully I am clear for everyone, thanks!
I figured it out! What I didn't do is put a width constraint on the custom cell I created! Duh!
I'd really appreciate if someone can light up some ideas here. I've been trying to fix this for weeks now (not kidding).
I have a "to do list" using a collectionView where I hide the rows that were completed and move them to the end of the list. I then unhide the items if needed with a button. The collectionView looks exactly as a tableView with one item(cell) per row.
When the items are hidden the collectionView has a lot of empty scrolling space at the bottom instead of automatically deleting the space used by the hidden rows since they technically are still there.
I'm trying to cut that empty space so the collectionView height would be equal to the amount of cells/rows left visible.
override func viewDidAppear(animated: Bool) {
if isCellHidden { //checking if cells are hidden
var heightOfSection0 = 95 + (42 * self.collectionView.numberOfItemsInSection(0))
println(self.collectionView.contentSize.heigh) //returns 2350
self.collectionView.contentSize.height = CGFloat(heightOfSection0)
println(heightOfSection0) //returns 1019
println(self.collectionView.contentSize.heigh) //returns 1019.0 which is correct but as soon as I scroll down it resets to it's original size (2350) and let's me scroll through that empty space...
}}
If I try to read the collectionView height immediately after setting this, it displays the correct value but as soon as I try to scroll down it resets back to it's original height. I also tried disabling the auto layout and it doesn't make a difference
You should not manage contentSize directly - return appropriate number of items to be displayed from collectionView(_:numberOfItemsInSection:) instead (i.e. do not count your "hidden" cells).
You can use sizeForItemAtIndexPath: to change the size of collection view cell.
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
var numberOfCellInRow : Int = 3
var padding : Int = 5
var collectionCellWidth : CGFloat = (self.view.frame.size.width/CGFloat(numberOfCellInRow)) - CGFloat(padding)
return CGSize(width: collectionCellWidth , height: collectionCellWidth)
}
Changing UI stuff in viewDidAppear() in theory is a good place to do so. However, at this point, you can't be sure that auto layout has operated on the subview that you're manipulating.
You can check when it has and when it's safe to manipulate the frame, by subclassing it and overriding layoutSubviews()
I had a similar issue here: https://stackoverflow.com/a/30446125/4396258