I've been working on this problem for probably 24 hours now and I can't seem to find out how you're intended by Apple to set the dynamic height of a CollectionViewCell.
This is where I'm completely lost:
1.) I need each Cell's frame size in my UICollectionView to be determined by the UILabel's text within each cell. The UILabel's frame is constrained to the edges of the cell, so simply grabbing the UILabel.frame's height gives me the precise value I need.
2.) 1 BIG PROBLEM, the UICollectionView's sizeForItemAtIndexPath's function is executed first before the UILabels are even populated in the UICollectionView's cellForItemAtIndexPath function.
Below is a hack I've tried which failed misrably:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MsgCell", forIndexPath: indexPath) as! CustomCollectionViewCell
cell.txtLabel.text = dummyChatFeed[indexPath.row].messageBody
cell.txtLabel.sizeToFit()
cell.lineBreakMode = NSLineBreakMode.ByWordWrapping
// store the value of this proper label height back into the array:
dummyChatFeed[indexPath.row].frameHeight = cell.txtLabel.frame.height
return cell
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
// grab that proper label height again:
var theHeightThatFits = dummyChatFeed[indexPath.row].frameHeight
return CGSize(width: Double(self.view.frame.width), height: Double(theHeightThatFits))
}
The dummyChatFeed is an NSObject Class which contains only these 4 properties:
var messageBody = ""
var author = ""
var frameHeight = CGFloat(0)
var indexPathRow = 0
This solution I've tried barely works, and it's buggy as hell since the height of the Cell depends on the label within it. But what is the best way to go from here?
I thought initially that it's best to keep an array of objects with one property of the object that stores the label text while another property of the object should store the value of the recommended height, but I've just learned the hard way that it would never work.
Am I suppose to programmatically declare a UILabel within my dummyChatFeed and calculate the width and height from there based on text and font?
I need my app to be backwards compatible with iOS 8.4 so I can't use UIStackViews.
Related
I have a UICollectionView, which initial height is 50.
I want to update it when I know the size of the cell.
For what I do:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let categoriesCount = 16
let numberOfItemsInRow = 3
let numberOfRows = ceil(CGFloat(categoriesCount) / CGFloat(numberOfItemsInRow))
let newHeight = (numberOfRows * cell.frame.size.height)
collectionHeight.constant = newHeight
collectionView.collectionViewLayout.invalidateLayout()
}
But despite of calling invalidateLayout - the height of the collectionView stays the same - 50.
But when I switch screens it shows the correct height when I open that screen with UICollectionView for the second time.
What I am doing wrong and how can I update the height of my UICollectionView?
I even tried collectionView.updateConstraints() after the collectionView.collectionViewLayout.invalidateLayout but still no luck
UPDATE
I've tried also
collectionView.layoutIfNeeded()
self.superview?.layoutIfNeeded()
but no result.
For this, you might have to call the self.view.layoutIfNeeded() method to force the view to update it's layout.
As per the documentation:
...this method lays out the view subtree starting at the root.
hence, updating your view
For more info, you can read this thread where you can pick bits and pieces to add to your solution.
Instead of trying to update the collectionView later based on the size of the cell, can we not use estimated height adjustment of the cell.
Change the Estimated size option in size inspector of collection view to "None" from "Automatic".
See the Image Below for reference.
enter image description here
I tried many answers given in different threads, none worked in my case but this one.
Check https://stackoverflow.com/a/59783595/11806784 this thread for reference.
I am trying to place UICollectionView inside UICollectionViewCell. I have prepared my cell in xib file using autolayout, same for cells inside collectionView.
But what about autolayout?
Some of my UICollectionViews inside my cell are different size that the others because of its content so I fail using sizeForItemAt indexPath: to calculate proper size.
Is there any fairly simple way to do this using autolayout, different from changing this main UICollectionView to UITableView with UITableViewAutomaticDimension as a heightForRowAt indexPath:?
EDIT:
To explain everything i've done so far to make this work i'll provide a cell layout:
and some algorithm to calculate cell's size:
First of all i declared an array of cells heights inside my UIViewController subclass like so:
fileprivate var cellsHeight: [CGFloat] = [].
After that inside UICollectionView delegate method collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell i am populating this array with height of each individual collectionView content size like so:
let cell = collectionView.dequeueReusableCell...
if cell.collectionView != nil {
cellsHeight.append(cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
}
And then in inside collectionView(_ collectionView: UICollectionView, sizeForItemAt indexPath: IndexPath) -> CGSize i am calculating each label size using boundingRect method, adding up some padding and previously fetched collectionView height.
But i think this is a pretty overkill.
The app crashes when calling dequeueReusableCellWithReuseIdentifier inside of collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize.
Note: this question is not a duplicate because dequeueReusableCellWithReuseIdentifier works elsewhere in the UICollectionView, just not within a specific function.
There is no exception message, and Xcode only highlights assembly code so unsure where the problem lies.
The code is below.
The goal is to make the cell height dynamic while the width matches the width of the UICollectionView, much like cells in a UITableView.
1) Why does it crash on dequeueReusableCellWithReuseIdentifier?
2) If you can't use dequeueReusableCellWithReuseIdentifier, how else can you dynamically determine the intrinsic height of the cell?
3) Is there a better way to mimic the sizing properties of a UITableViewCell (i.e., same width as superview but dynamic height)? There are many SO posts on this topic but none provide very clean solutions.
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(MessageCellIdentifier, forIndexPath: indexPath) as! MessageCell
let cellSize = CGSize(width: view.frame.width, height: cell.frame.height)
return cellSize
}
Closer to understanding the crash
This is a 1:1 replacement of UITVC without pissing Apple off.
First, this is where your callback gets used.
// if delegate implements size delegate, query it for all items
if (implementsSizeDelegate) {
for (NSInteger item = 0; item < numberOfItems; item++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:(NSInteger)section];
CGSize itemSize = implementsSizeDelegate ? [flowDataSource collectionView:self.collectionView layout:self sizeForItemAtIndexPath:indexPath] : self.itemSize;
PSTGridLayoutItem *layoutItem = [layoutSection addItem];
layoutItem.itemFrame = (CGRect){.size=itemSize};
}
https://github.com/steipete/PSTCollectionView/blob/master/PSTCollectionView/PSTCollectionView.m
Look at attributes assignment and the very last line...
- (id)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath {
// de-queue cell (if available)
NSMutableArray *reusableCells = _cellReuseQueues[identifier];
PSTCollectionViewCell *cell = [reusableCells lastObject];
PSTCollectionViewLayoutAttributes *attributes = [self.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
// ... IMPL ...
[cell applyLayoutAttributes:attributes];
return cell;
}
There is much code to go through but there is at least one path where you can fall into infinite recursion...
Edit
From the link I posted below...
Since collectionView:layout:sizeForItemAtIndexPath: is called before
cellForItemAtIndexPath:, so we need to initialize a cell and let
system use auto layout to calculate height for us. To avoid memory
leak, we use a dictionary to cache the cells that are off screen (not
shown on screen)
In collectionView:layout:sizeForItemAtIndexPath:, first create or retrieve a cell
var cell: MyCollectionViewCell? = self.offscreenCells[reuseIdentifier] as? MyCollectionViewCell
if cell == nil {
cell = NSBundle.mainBundle().loadNibNamed("MyCollectionViewCell", owner: self, options: nil)[0] as? MyCollectionViewCell
self.offscreenCells[reuseIdentifier] = cell
}
Once a cell is initialized, its size is determined by size in xib file, thus, we need configure texts in cell and layoutSubviews, this will let system recalculate the size of cell
// Config cell and let system determine size
cell!.configCell(titleData[indexPath.item], content: contentData[indexPath.item], titleFont: fontArray[indexPath.item] as String, contentFont: fontArray[indexPath.item] as String)
// Cell's size is determined in nib file, need to set it's width (in this case), and inside, use this cell's width to set label's preferredMaxLayoutWidth, thus, height can be determined, this size will be returned for real cell initialization
cell!.bounds = CGRectMake(0, 0, targetWidth, cell!.bounds.height)
cell!.contentView.bounds = cell!.bounds
// Layout subviews, this will let labels on this cell to set preferredMaxLayoutWidth
cell!.setNeedsLayout()
cell!.layoutIfNeeded()
Once cell is updated, call var size =
cell!.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
to get the size for this cell.
In cellForItemAtIndexPath:, cell also need configured and layout its
subviews
Orig
Well, it doesn't crash when calling the method you named (half of it), it crashes when calling
dequeueReusableCellWithReuseIdentifier(MessageCellIdentifier,
forIndexPath: indexPath) as! MessageCell
The problem I see with it is that you are trying to call a method that needs the method you are calling it from to give you an answer. It should produce a stack overflow.
I'm not saying it's impossible but you aren't overriding enough or the right methods I believe.
Check out the https://github.com/honghaoz/Dynamic-Collection-View-Cell-With-Auto-Layout-Demo/blob/master/README.md for an example of what you want, and/or post more code.
The block of code below, which alters the lblBody's frame size seems to be having zero effect on my UITableViewCells.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ChatCell", forIndexPath: indexPath) as! ChatCellTableView
//cell.lblBody.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.lblBody.text = msgSections[indexPath.section].msg[indexPath.row].text
cell.lblUsername.text = msgSections[indexPath.section].msg[indexPath.row].username
cell.lblTime.text = msgSections[indexPath.section].msg[indexPath.row].time
cell.lblBody.frame.size.height = 900
cell.lblBody.textColor = UIColor.greenColor()
return cell
}
When I build and run my app, my UITableViewCell's labels are the exact same size as they are in the storyboard editor. I already removed all constraints with the "clear constrains" command, but for some reason my uilabels still set their size based off of the storyboard.
I set the text color to an ugly green just as a sanity check. My uilabel's colors are green, their text is proper, everything but the label's size seems to be effected.
I need this because the array which returns the text to be shown in the UILable also contains the precise width and height the labels will need to work properly.
UPDATE:
Another curious thing I've found is that in RAM the heights of my lblBody's are correct when I print the following:
print("THE CURRENT HEIGHT: ")
print(cell.lblBody.frame.size.height)
return cell
When I build and run the app and scroll through the cells, I get the following output:
THE CURRENT HEIGHT:
900.0
THE CURRENT HEIGHT:
900.0
THE CURRENT HEIGHT:
900.0
THE CURRENT HEIGHT:
900.0
So it looks like the height is being properly set within the UILabel's properties, but I don't see UILabels with the height of 900. All of my UILabel's sizes are the same as they appear in the story board editor. Reloading/refreshing the tableview's data does not fix the issue here.
The actual drawing of your cell occurs after cellForRowAtIndexPath occurs. To have control after bounds have been set, I would suggest you take a look at modifying the label within the override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) method.
I'm finding friction when trying to create responsive / adaptive UICollectionViewCells in the UIStoryboard.
The issue I'm seeing is that you don't seem to be able to set the Cell Size per Size Class and I'm trying to ascertain the right approach to this. I've designed the cells to adjust to their containers, so that they should autosize regardless of size class. This mostly works in that if I change the size class, select my cell view and do Update Frames then they all resize to fit their new size. However it's a one shot deal, if I go back to the Any/Any size class then I'm still seeing that resized version.
Here's what I'm aware I could try:
Create multiple cells, with fixed dimensions, one per size class and in the Storyboard view. I could then only use the right one at runtime but I could then see them at design time.
I could create a collection view Per Size class, each one only being installed for that size. This would work, but would be a pain to manage the multiple UICollectionViews
Create my interface and/or constraints programmatically (losing visibility of the design).
I'm hoping this is a solved scenario and I'm just missing something, but I'm aware that it could be that the IB tools don't match the code at this point.
The solution I came up with was just to implement the UICollectionViewDelegateFlowLayout and implement the sizeForItemAtIndexPath method.
This means that the cell dimensions can be set to match the available Size Class dimensions.
This still isn't ideal as you can't see the changes in the storyboard and you can't create a universal design and see it in each of the different formats.
I'm still hoping someone has a better option.
Here's a similar solution coded-out in Swift. I just styled both of my cells in the storyboard and leave them viewable for any size class combination. When the trait collection changes I update the cellSize and cellReuseID I want to use and tell the collectionView to reload all the visible cells. Then
collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
and
override func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
(not shown in sample code) are called which lets me update the size of the cell and update the cell's storyboard styling. So not entirely done in storyboard, but good enough until more support is provided in Xcode.
struct MyCollectionViewConstants{
static let CELL_ANY_ANY_REUSE_ID = "cell";
static let CELL_COMPACT_REGULAR_REUSE_ID = "cellSmall"
static let CELL_ANY_ANY_SIZE = 100;
static let CELL_COMPACT_REGULAR_SIZE = 70;
}
class MyCollectionView: UICollectionViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var cellSize = MyCollectionViewConstants.CELL_ANY_ANY_SIZE
var cellReuseID = MyCollectionViewConstants.CELL_ANY_ANY_REUSE_ID
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize{
return CGSize(width: cellSize, height: cellSize)
}
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact
&& self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClass.Regular){
cellSize = MyCollectionViewConstants.CELL_COMPACT_REGULAR_SIZE
cellReuseID = MyCollectionViewConstants.CELL_COMPACT_REGULAR_REUSE_ID
} else {
cellSize = MyCollectionViewConstants.CELL_ANY_ANY_SIZE
cellReuseID = MyCollectionViewConstants.CELL_ANY_ANY_REUSE_ID
}
self.collectionView.reloadItemsAtIndexPaths(
self.collectionView.indexPathsForVisibleItems())
}
}
I was stuck with same problem after implementing size class(iPad and iPhone).Well, I figured out a solution. Hope it helps!
Implement UICollectionViewDelegateFlowLayout.
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
var device = UIDevice.currentDevice().model
var cellSize:CGSize = CGSizeMake(155, 109)
if (device == "iPad" || device == "iPad Simulator") {
cellSize = CGSizeMake(240, 220)
}
return cellSize
}
Swift 4
Hi Fellow Developers,
This is easy to do if the height and width of the UICollectionViewCell are same.
Steps
1. Import ** UICollectionViewDelegateFlowLayout**
2. Add the sizeForItem IndexPath method of UICOllectionView as follows
func collectionView(_ collectionView : UICollectionView,layout collectionViewLayout:UICollectionViewLayout,sizeForItemAt indexPath:IndexPath) -> CGSize {
return CGSize(width: collectionVw.frame.size.height, height: collectionVw.frame.size.height)
}
Note: What happening is you are setting the height of the UICollectionView as height and width of the UICollectionViewCell
Happy Coding :)