CollectionView estimatedItemSize not working as expected - ios

Im trying to use estimatedItemSize to dynamically resize collectionView cells to fit they're content tower I'm having a few problems with different device sizes and particularly the width not resizing, its just staying the same size as it is in interface builder.
I have done my Interface Builder stuff on an iPhone 7 so the screen width is 375. In my storyBoard i have the following.
outerCollectionView pinned to all 4 sides of the superView with 0 margin
outerCollectionView Cell of w: 339 h:550
Inside this cell i have:
innerCollectionView pinned to all 4 sides of the cell with 0 margin
innerCollectionView constant w: 339 h: 550 added in IB
Inside my DataSource and Delegate class i have defined my flow layout in the viewDidLoad function:
let width = UIScreen.main.bounds.size.width
let height = UIScreen.main.bounds.size.height
let flow = outerCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
flow.estimatedItemSize = CGSize(width: width - ((width / 18.75)), height: height / 4)
flow.minimumLineSpacing = (width / 37.5)
flow.sectionInset = UIEdgeInsets(top: (width / 37.5), left: (width / 37.5), bottom: (width / 37.5), right: (width / 37.5))
And inside the custom cell class that holds the "inner" collection i set some of these constant constraints and define the flow layout to size the items:
// This is the correct screen size
// let width = innerCollectionView.frame.size.width
let width = UIScreen.main.bounds.size.width
let flow = innerCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
flow.sectionInset = UIEdgeInsetsMake((width / 37.5), (width / 37.5), (width / 37.5), (width / 37.5))
flow.itemSize = CGSize(width: (width / 6 - (width / 37.5)), height: (width / 6 - (width / 37.5)))
flow.minimumInteritemSpacing = (width / 37.5)
flow.minimumLineSpacing = (width / 37.5)
innerCollectionHeight.constant = innerCollectionView.collectionViewLayout.collectionViewContentSize.height
innerCollectionWidth.constant = innerCollectionView.collectionViewLayout.collectionViewContentSize.width
Now all this works fine and the cells resize vertically to fit my content nicely on the iPhone 7 simulator, showing a single column cells correctly populated. But when i step up a device size does not adjust the width of the cell. And when i go down to a smaller device i get a whole bunch of Xcode errors about how the width of the cell is is to big because it hasn't been resized, but nothing displays. (some pics below)
2017-02-16 12:53:49.632 ParseStarterProject-Swift[26279:394906] The behavior of the UICollectionViewFlowLayout is not defined because:
2017-02-16 12:53:49.632 ParseStarterProject-Swift[26279:394906] 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.
2017-02-16 12:53:49.632 ParseStarterProject-Swift[26279:394906] Please check the values returned by the delegate.
2017-02-16 12:53:49.633 ParseStarterProject-Swift[26279:394906] The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7be38920>, and it is attached to <UICollectionView: 0x7db03600; frame = (0 0; 320 504); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x7d61a550>; layer = <CALayer: 0x7be95880>; contentOffset: {0, 0}; contentSize: {320, 20}> collection view layout: <UICollectionViewFlowLayout: 0x7be38920>.
So obviously i cant use
flow.itemSize =
to set the size of the outer cell because i need the estimated size to adjust the vertical length and hug the content. I just cant figure out why the width isnt rising to suit the screen size??
Large sized device - margin too large: CV width 339
Iphone 7 as i built it - margins normal: CV width 339
---- EDIT -----
tried overriding szeForItemAt but there is no change.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = self.view.frame.size.width
let height = self.view.frame.size.height
if collectionView == self.outerCollectionView {
return CGSize(width: width - 100, height: height / 4)
} else {
return CGSize(width: (width / 6 - (width / 75)), height: (width / 6 - (width / 75)))
}
}
---- EDIT ----
I can edit the preferred item seize of the cell. Using the compressed size option for height and setting my own size for width, (width / 18.7) is the width minus left and right inset previously defined
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let attributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes
let width = UIScreen.main.bounds.size.width
let desiredWidth = width - (width / 18.75)
//let desiredWidth = systemLayoutSizeFitting(UILayoutFittingExpandedSize).width
let desiredHeight = systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
attributes.frame.size.width = desiredWidth
attributes.frame.size.height = desiredHeight
return attributes
}
Bu this stuffs up the constraints as shown below in the image, which i think might just be a script i have running to fillet the top corners of UIViews. But its also stuffing up the cells as it cramming more per line.
Smaller screen, similar problem with the cells:

The solution is to implement UICollectionViewDelegateFlowLayout and call sizeForItemAt like this:
extension YourClass: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout:UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
//return size needed
return CGSize(width: (width / 6 - (width / 37.5)), height: (width / 6 - (width / 37.5)))
}
At runtime the returned size will be the one giving the item Size.
Note: implementing sizeForItemAt without UICollectionViewDelegateFlowLayout will not succeed

Related

Struggling to make collection view height fit content

I have a bit of a special scenario and no matter what StackOverflow answer I find, it doesn't seem to work for me. I have a collection view with a dynamic number of cells (anywhere between 2-3 cells). However, the height of this collection view will always be the same, no matter how many cells there are.
Here is the logic I have for setting the cell heights:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// The cell's width and height, to be calculated
var cellWidth: CGFloat = 0.0, cellHeight: CGFloat = 0.0
// The number of cells to show, 2-3
let count = numberOfCellsToShow
// The width of the collection view
let collectionViewWidth = collectionView.bounds.size.width
// Calculate the maximum height for collection view (16:9 ratio)
let collectionViewMaxHeight = collectionViewWidth / (16 / 9)
// Get the width and height of each cell, with 4dp of spacing between cells
if (count == 2) {
cellWidth = (collectionViewWidth - CGFloat(4)) / 2
cellHeight = collectionViewMaxHeight
} else if (count == 3) {
cellWidth = (collectionViewWidth - (CGFloat(4) * 2)) / 3
cellHeight = collectionViewMaxHeight
}
return CGSize(width: cellWidth, height: cellHeight)
}
Now, I want to make sure that the height of the collection view fits the content, but this is where I'm struggling to make it happen.
My collection view is inside a stack view, with all constraints set to the edges of the stack view.
What do I need to do to set the height of my collection view to fit the contents?

2 column collectionview sizing problem in some iphones

I am using this code
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let padding: CGFloat = 50
let collectionViewSize = collectionView.frame.size.width - padding
return CGSize(width: collectionViewSize/2, height: collectionViewSize/2)
}
I am able to get a 2 column collection view on all iPhones except iPhone X and iphone XR, I don't know why
How to force 2 columns for all iPhones?
You can set layout of your collectionView by creating new layout and set it's itemSize, minimumInteritemSpacing and minimumLineSpacing and then assign new layout as collectionView.collectionViewLayout:
func setCollectionViewLayout(withPadding padding: CGFloat) {
let layout = UICollectionViewFlowLayout()
let size = (collectionView.frame.width - padding) / 2
layout.itemSize = CGSize(width: size, height: size)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView.collectionViewLayout = layout
}
and then call this method in viewDidLayoutSubviews (this is moment when frames are loaded and you can calculate with collectionView's frame)
override func viewDidLayoutSubviews() {
setCollectionViewLayout(withPadding: 50)
}
Note: I would recommend you to set leading and trailing constraints of collectionView to constant 25 instead of using padding
I suggest that you calculate width according to safeAreaLayoytGuide and, if you're using UICollectionViewFlowLayout, sectionInset. For UICollectionViewFlowLayout the following code will calculate proper width:
let sectionInset = (collectionViewLayout as! UICollectionViewFlowLayout).sectionInset
let width = collectionView.safeAreaLayoutGuide.layoutFrame.width
- sectionInset.left
- sectionInset.right
- collectionView.contentInset.left
- collectionView.contentInset.right
If you need two columns, than item width will be calculated like that:
let space: CGFloat = 10.0
let itemSize = CGSize(width: (width - space) / 2, height: 100 /*DESIRED HEIGHT*/)

Calculating height of UICollectionViewCell with text only

trying to calculate height of a cell with specified width and cannot make it right. Here is a snippet. There are two columns specified by the custom layout which knows the column width.
let cell = TextNoteCell2.loadFromNib()
var frame = cell.frame
frame.size.width = columnWidth // 187.5
frame.size.height = 0 // it does not work either without this line.
cell.frame = frame
cell.update(text: note.text)
cell.contentView.layoutIfNeeded()
let size = cell.contentView.systemLayoutSizeFitting(CGSize(width: columnWidth, height: 0)) // 251.5 x 52.5
print(cell) // 187.5 x 0
return size.height
Both size and cell.frame are incorrect.
Cell has a text label inside with 16px margins on each label edge.
Thank you in advance.
To calculate the size for a UILabel to fully display the given text, i would add a helper as below,
extension UILabel {
public static func estimatedSize(_ text: String, targetSize: CGSize = .zero) -> CGSize {
let label = UILabel(frame: .zero)
label.numberOfLines = 0
label.text = text
return label.sizeThatFits(targetSize)
}
}
Now that you know how much size is required for your text, you can calculate the cell size by adding the margins you specified in the cell i.e 16.0 on each side so, the calculation should be as below,
let intrinsicMargin: CGFloat = 16.0 + 16.0
let targetWidth: CGFloat = 187.0 - intrinsicMargin
let labelSize = UILabel.estimatedSize(note.text, targetSize: CGSize(width: targetWidth, height: 0))
let cellSize = CGSize(width: labelSize.width + intrinsicMargin, height: labelSize.height + intrinsicMargin)
Hope you will get the required results. One more improvement would be to calculate the width based on the screen size and number of columns instead of hard coded 187.0
That cell you are loading from a nib has no view to be placed in, so it has an incorrect frame.
You need to either manually add it to a view, then measure it, or you'll need to dequeu it from the collectionView so it's already within a container view
For Swift 4.2 updated answer is to handle height and width of uicollectionview Cell on the basis of uilabel text
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let size = (self.FILTERTitles[indexPath.row] as NSString).size(withAttributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14.0)])
return CGSize(width: size.width + 38.0, height: size.height + 25.0)
}

UICollectionView with zero cell spacing

I've followed this answer and try to implement 5 cells per row and it's working great when I check on iPhone 6 & iPhone SE as below.
But the problem occures when I try to run it on iPhone 6 Plus. Can anyone help me out on figuring out the issue please?
This is my code.
screenSize = UIScreen.main.bounds
screenWidth = screenSize.width
screenHeight = screenSize.height
let itemWidth : CGFloat = (screenWidth / 5)
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.itemSize = CGSize(width: itemWidth, height: itemWidth)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.sectionInset = UIEdgeInsets(top: 10.0, left: 0, bottom: 10.0, right: 0)
collectionView!.collectionViewLayout = layout
It has to be
let itemWidth : CGFloat = (screenWidth / 5.0)
So the result will not be rounded.
Updated
Please make sure if you use storyboard to create your UICollectionView, remember to set autolayout to the collection view's size so that it is updated to whatever current screen size is.
Update 2
If you use storyboard there is no need to create a UICollectionViewFlowLayout. You can set the insets and spacings from storyboard.
Then in your .m file implement this to determine item's size.
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return yourDesiredSize;
}
To fix the blank spaces the size of each cell should be a round number. Then the difference between the sum of rounded numbers and the size of collectionView can be equally distributed or put in one cell.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// To avoid white space inbetween cells we're rounding the width of each cell
var width = CGFloat(floorf(Float(screenWidth/CGFloat(objects.count))))
if indexPath.row == 0 {
// Because we're rounding the width of each cell the cells don't cover the space completly, so we're making the first cell a few pixels wider to make sure we fill everything
width = screenWidth - CGFloat(objects.count-1)*width
}
return CGSize(width: width, height: collectionView.bounds.height)
}

How to adjust the height of the header of a tableview in swift?

I have a UITableViewController. This table view has an header view.
I use main.storyboard with autolayout to set my controller.
In my main.storyboard, for w:Any h:Any, the preview of my view controller is set with the default size 600x600. Inside, my header view is set to 600x200.
In my header view, I have an imageView set to the Aspect Fill mode:
The constraints :
In the assets, my image has #2x and #3x sizes.
header-image#2x.png -> 750x498
header-image#3x.png -> 1242x825
When I compile, in the simulator I obtain for iPhone 5:
For iPhone 6:
For iPhone 6+:
For the iPhone 5, the image starts below the status bar. I don't
understand why?
For the iPhone 6 and iPhone 6+, the image crops the first cell of my table view. And the top of the image isn't adjust with the top of the view.
I don't know how to adjust the height of the tableHeaderView to the height of my image, because this height depends on the device.
I have tried to set programmatically the frame of the header, in vain:
var frame:CGRect!
frame.size.width = self.bgHeaderImageView.image?.size.width
frame.size.height = self.bgHeaderImageView.image?.size.height
self.tableView.tableHeaderView?.frame = frame
I got 2 errors : "Value optional type CGFloat? not unwrapped"
And if I correct with
var frame:CGRect!
frame.size.width = self.bgHeaderImageView.image?.size.width!
frame.size.height = self.bgHeaderImageView.image?.size.height!
self.tableView.tableHeaderView?.frame = frame
I got 2 errors : "Operand of postfix ! should have optional type"
Is it to possible to adjust the size in the storyboard directly and not programmatically ?
I'm probably missing something here...
The UITableView tableHeaderView has no option for aspect ratio and autolayout usage of the tableHeaderView is limited in InterfaceBuilder. There are many ways to achieve a dynamic height of the tableHeaderView. If you want to have a tableHeaderView height based on the ratio of a header image you can use:
let width = UIScreen.mainScreen().bounds.size.width
self.height = (width/640) * 209 //calculate new height
self.tableView.tableHeaderView?.frame.size = CGSize(width: self.tableView.tableHeaderView!.frame.size.width, height: self.height)
I found a solution, it's not very clean, but it works.
var kTableHeaderHeight:CGFloat! //expected height of the header tableview
var image: UIImage = UIImage(named: "my-image")!
var frame:CGRect!=self.tableView.tableHeaderView?.frame
if UIScreen.mainScreen().bounds.size.width <= 320 { //iPhone 5 and less
frame.size.width = 320
frame.size.height = 212
} else { //iPhone 6 #2x and iPhone 6+ #3x
frame.size.width = image.size.width
frame.size.height = image.size.height
}
self.tableView.tableHeaderView?.frame = frame
Try this
self.edgesForExtendedLayout = UIRectEdge.None
self.automaticallyAdjustsScrollViewInsets = false
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
And this too
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 0
{
return 1
}
else
{
return 40; // your other headers height value
}
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0
{
// Note CGFloat.min for swift
// For Objective-c CGFLOAT_MIN
let headerView = UIView.init(frame: CGRectMake(0.0, 0.0, self.tableView.bounds.size.width, CGFloat.min))
return headerView
}
else
{
// Construct your other headers here
return UIView()
}
}

Resources