Here is an design issue in the app that uses AutoLayout, UICollectionView and UICollectionViewCell that has automatically resizable width & height depending on AutoLayout constraints and its content (some text).
It is a UITableView list like, with each cell that has it's own width & height calculated separately for each row dependant on its content. It is more like iOS Messages build in app (or WhatsUp).
It is obvious that app should make use of func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize.
Issue is that within that method, app cannot call func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell nor dequeueReusableCellWithReuseIdentifier(identifier: String, forIndexPath indexPath: NSIndexPath!) -> AnyObject to instantiate cell, populate it with specific content and calculate its width & height. Trying to do that will result in an indefinite recursion calls or some other type of app crash (at least in iOS 8.3).
The closest way to fix this situation seems to copy definition of the cell into view hierarchy to let Auto-layout resize "cell" automatically (like cell to have the same width as parent collection view), so app can configure cell with specific content and calculate its size. This should definitely not be the only way to fix it because of duplicated resources.
All of that is connected with setting UILabel.preferredMaxLayoutWidth to some value that should be Auto-Layout controllable (not hardcoded) that could depend on screen width & height or at least setup by Auto-layout constraint definition, so app can get multiline UILabel intrinsic size calculated.
I would not like to instantiate cells from XIB file since Storyboards should be today's industry standard and I would like to have as less intervention in code.
EDIT:
The basic code that cannot be run is listed bellow. So only instantiating variable cellProbe (not used) crashes the app. Without that call, app runs smoothly.
var onceToken: dispatch_once_t = 0
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
dispatch_once(&onceToken) {
let cellProbe = collectionView.dequeueReusableCellWithReuseIdentifier("first", forIndexPath: NSIndexPath(forRow: 0, inSection: 0)) as! UICollectionViewCell
}
return CGSize(width: 200,height: 50)
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("first", forIndexPath: NSIndexPath(forRow: 0, inSection: 0)) as! UICollectionViewCell
return cell
}
}
If you are using constraints, you don't need to set a size per cell. So you should remove your delegate method func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
Instead you need to set a size in estimatedItemSize by setting this to a value other than CGSizeZero you are telling the layout that you don't know the exact size yet. The layout will then ask each cell for it's size and it should be calculated when it's required.
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.estimatedItemSize = someReasonableEstimatedSize()
Just like when dynamically size Table View Cell height using Auto Layout you don't need call
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
in
optional func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
the proper way is create a local static cell for height calculation like a class method of the custom cell
+ (CGFloat)cellHeightForContent:(id)content tableView:(UITableView *)tableView {
static CustomCell *cell;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
cell = [tableView dequeueReusableCellWithIdentifier:[CustomCell cellIdentifier]];
});
Item *item = (Item *)content;
configureBasicCell(cell, item);
[cell setNeedsLayout];
[cell layoutIfNeeded];
CGSize size = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height + 1.0f; // Add 1.0f for the cell separator height
}
and I seriously doubt storyboard is some industry standard. xib is the best way to create custom cell like view including tableViewCell and CollectionViewCell
To calculate the CollectionView cell's size dynamically, just set the flow layout property to any arbitrary value:
let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
flowLayout.estimatedItemSize = CGSize(width: 0, height: 0)
Related
Hello fellow overflowers so I set up a collectionView and configure it but when I run the simulator I get a lot of cells just mixed with each other like in the first picture.
I have added the identifier at the storyboards cell also created the class for the cell but I can't seem to know what the problem might be since it never happened.
How I want my cell to be I will show on the second picture.
first picture
second picture
the code:
extension dashboardViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int{
return 15
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "dashboardCollectionViewCell", for: indexPath) as? dashboardCollectionViewCell else {return UICollectionViewCell()}
return cell
}
}
By the image provided, it seems you haven't set the item size of the cell
in horizontal layout as well as the minimum spacing between each cell. You can do that (set item size) by calling
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {}
this method from UICollectionViewDelegateFlowLayout.
You can also do this using storyboard.
Size Inspector
Seems that your collection view cell's size has not correct set.
If your are using UICollectionViewFlowLayout, maybe you can check the layout object's itemSize property.
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
...
// Set a suitable CGSize for the `itemSize` property
// In this example I set it to 100pt * 50pt
layout.itemSize = CGSizeMake(100, 50);
...
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
I am trying to create simple educational app about movies, and I need to create movie frames horizontal scroller.
I create collection view for that:
But if I build it for another target (iPhone 6s Plus for example), I have this:
How to solve this problem?
My scene controller code (Swift 3):
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.backgroundColor = UIColor.black
// Configure the cell
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
I think that you can try to implement the method of UICollectionView delegate:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSizeMake(yourHeight, self.view.frame.size.width)
}
in this way, your collectionView-cell inside in tableView will appear as big as the screen.
First I want to say something about UITableViewAutomaticDimension you use, in case you not exactly understand how it works.
Your expectations probably are, that UITableViewAutomaticDimension will infer tableView to make a cell with size of entire tableView, but it doesn't work this way.
UITableViewAutomaticDimension is just infers tableView, that inner size of your cell will be used as a returned here; so if you add some constraints, views with constraints, or whatever inside your tV, its size will be used.
and second - your comment doesn't make sense at all for me, because your posted code isn't related to cell of collectionView and I can't comment anything related to size of its cells
EDITED:
to make all cells with width of your screen, return correct size from delegate method of collectionViewFlowLayout - sizeForItem
I am trying to make my cell height size fit with label. The thing is that the label text is set in the cellForItemAtIndexPath, and if i have understood correct, sizeForItemAtIndexPath runs before the cellForItemAtIndexPath. This is something i have tried so far:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
let imageCell = collectionView.dequeueReusableCellWithReuseIdentifier("imageCell", forIndexPath: indexPath) as? CollectionViewCell
imageCell!.imageText.text = post.text // This set the UICollectionView Cell text
imageCell!.frame.size.height = (imageCell!.frame.size.height) + (imageCell!.imageText.frame.size.height)
return imageCell!
}
I am not using Auto Layout.
Any suggestions why the cell height not changes depending on the label?
This used to be an annoying problem to solve, but it gets substantially easier if you're able to use Auto Layout. See this answer for a thorough walkthrough.
Even if sizeForItemAtIndexPath runs first, that's the place to set the size of the cell. cellForItemAtIndexPath cannot change the size.
In sizeForItemAtIndexPath you need to find out the image size for each cell, then return that size.
Something like this:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let imageCell = collectionView.cellForItemAtIndexPath(indexPath) as! CollectionViewCell
let size = CGSize(width: imageCell.frame.width, height: imageCell.frame.size.height + imageCell.imageText.frame.size.height)
return size
}
I have a storyboard, consisting of a single UICollectionView with multiple cells, each of varying height. The first cell takes the height from the UICollectionViewDelegateFlowLayout:
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
.. but I'd like the second cell to be shorter.
I've placed two UIStackViews inside a "master" UIStackView inside the cell, and each of the inner UIStackViews has one or more labels, like this:
cell
--> stackView (master)
--> stackView (1)
--> label
--> stackView (2)
--> label
--> label (etc)
.. in the hope that the UIStackView would make the cell height dynamic, but it doesn't. It takes the height from the UICollectionViewDelegateFlowLayout as before.
How should I be doing this?
You need to compute the size of the content for the CollectionViewCell and return it to the sizeForItemAt function.
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
// Create an instance of the `FooCollectionViewCell`, either from nib file or from code.
// Here we assume `FooCollectionViewCell` is created from a FooCollectionViewCell.xib
let cell: FooCollectionViewCell = UINib(nibName: "FooCollectionViewCell", bundle: nil)
.instantiate(withOwner: nil, options: nil)
.first as! FooCollectionViewCell
// Configure the data for your `FooCollectionViewCell`
cell.stackView.addArrangedSubview(/*view1*/)
cell.stackView.addArrangedSubview(/*view2*/)
// Layout the collection view cell
cell.setNeedsLayout()
cell.layoutSubviews()
// Calculate the height of the collection view based on the content
let size = cell.contentView.systemLayoutSizeFitting(
CGSize(width: collectionView.bounds.width, height: 0),
withHorizontalFittingPriority: UILayoutPriorityRequired,
verticalFittingPriority: UILayoutPriorityFittingSizeLevel)
return size
}
With this, you will have a dynamic cell heights UICollectionView.
Further notes:
for configuration of the collection view cell, you can create a helper function func configure(someData: SomeData) on FooCollectionViewCell so that the code could be shared between the cellForItemAt function and sizeForItemAt function.
// Configure the data for your `FooCollectionViewCell`
cell.stackView.addArrangedSubview(/*view1*/)
cell.stackView.addArrangedSubview(/*view2*/)
For these two lines of code, it seems only needed if the UICollectionViewCell contains vertical UIStackView as subViews (likely a bug from Apple).
// Layout the collection view cell
cell.setNeedsLayout()
cell.layoutSubviews()
If you'd like to change the height of your cells you're going to have to change the height you return in the sizeForItemAtIndexPath. The stack views aren't going to have any effect here. Here's an example of what you can do:
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
if indexPath.row == 1 {
return CGSizeMake(width, height/2)
}
return CGSizeMake(width, height)
}
This will change the size of your cells at row 1. You can also use indexPath.section to choose sections. Hope this helps.
I've set the width of a cell(UICollectionViewCell) to be equal to the width of the UICollectionView and I'm trying to do exactly the same thing with the UILabel that is included inside that cell. I think the code below explains exactly what I'm trying to achieve. So i've read some question here in SO and also a couple of tutorials but I'm still not sure how I can achieve this.
In a couple of questions it was saying about using collectionViewLayout but I'm really struggling on how to use it within my code. Any ideas? Thank you!
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("myCell", forIndexPath: indexPath) as LocationViewCell
cell.locationLabel.text = "Hello there!"
// Set cell & label width to 100%
let collectionViewWidth = self.collectionView.bounds.size.width
cell.frame.size.width = collectionViewWidth // Works
cell.locationLabel.frame.size.width = collectionViewWidth // Does NOT
Update 1
So I added the following:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
// Set cell width to 100%
let collectionViewWidth = self.collectionView.bounds.size.width
return CGSize(width: collectionViewWidth, height: 35)
}
What happens is that when the view is loaded the UILabel's width is still small. If I go to another view and then return back then it's 100%. So I have to do something in the viewDidLoad() right? I'm already using self.collectionView.reloadData() but I guess that's only for data.
Update 2
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("locationCell", forIndexPath: indexPath) as LocationViewCell
cell.locationLabel.text = "Hello UILabel"
// Set cell width to 100%
let collectionViewWidth = self.collectionView.bounds.size.width
cell.frame.size.width = collectionViewWidth
cell.locationLabel.frame.size.width = collectionViewWidth
return cell
}
It doesn't work because by the time this method is called, the collection view already knows how big the cell should be because it has got it from the flow delegate method:
optional func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
This is where you should be setting the size of your cells.