I'm trying to create a collection view with cells that can autosize. previously i had used sizeForItemAtIndexPath but could not get the cell height exactly right for a textView with attributedString. I've decided to abandon that approach and use the auto-sizing feature. I have found some information here but mostly with objective-C. I am only familiar with Swift. Even so, I have picked through it and it is still not working.
What I have done so far is to include the following code in my viewDidLoad:
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(
width: self.view.frame.size.width, height: 100)
}
the cell width is showing correctly however all the cell heights are stuck at 100 regardless of the content.
Is there something else I need to do to allow the autosizing to kick in? I'm pretty sure my storyboard constraints are set up correctly.
In addition to setting the estimatedItemSize, you should also make sure that you are not providing an item size via a delegate or datasource method. The point of the self-sizing mechanism is that UIKit will use the estimated item size as its initial estimate, and then calculate the exact height based on the Auto Layout constraints you've configured on the cell's contentView. Also, you need a Base SDK of iOS 8 or later.
This repo reproduces Apple's example from the WWDC session where they introduced self-sizing cells.
One workaround might be to size the collectionView cells height and width in proportion to the screen size:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let deviceSize = UIScreen.mainScreen().bounds.size
//let cellSize = sqrt(Double(deviceSize.width * deviceSize.height) / (Double(33)))
let cellWidth = ((deviceSize.width / 2) - 10)
let cellHeight = (deviceSize.height / 4)
return CGSize(width: cellWidth , height: cellHeight)
}
Related
This question already has answers here:
AutoSizing cells: cell width equal to the CollectionView
(2 answers)
Closed 3 years ago.
I'm working on a rebuild of UI on mobile App. I've changed my tableView by CollectionView (for have two columns of cells)
I've achieve the work to have dynamic heigh on cell and that work perfectly, as you can see (iOS left and Android right) I'm trying to achieve the android UI with iOS.
Currently that what I have follow to achieve that :
https://www.vadimbulavin.com/collection-view-cells-self-sizing/
I've trying to set fixed width but that's automatically shape to the content.
I think that probably a little thing but I havent work from long time on iOS, so I'm looking for explain to achieve the fixed width size
The controller contain only that :
#IBOutlet weak var collectionViewFlowLayout: UICollectionViewFlowLayout!{
didSet {
self.collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
I havent do the 'max width' part of tutorial.
All label inside cell have left right constraint fixed, 0 lines and word wrap line break
Thank !
Have a nice day
Benjamin
Well, as i see from screenshot, you already have auto height on each cell.
Set fixed width for CollectionViewCell:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var height:CGFloat = 0.0
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
height = layout.itemSize.height
}
let screenSize = UIScreen.main
let wi = screenSize.bounds.size.width/2 // can calculate gap between two columns
return CGSize(width: wi, height: height)
}
You need to do a custom layout of class UICollectionViewLayout and set it to your collection view. There are a lot of Pinterest like collection view tutorials. You can check this
I don't know why it is so complicated to design cells that can adapt to its content. It shouldn't need that much code, I still don't understand why UIKit can't handle this properly.
Anyway, here is my issue (I have edited the whole post):
I have an UICollectionViewCell that contains an UITableView.
Here is my sizeForItem method :
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
var cellWidth: CGFloat = collectionView.bounds.size.width
var cellHeight: CGFloat = 0
let cellConfigurator = items[indexPath.item].cellConfigurator
if type(of: cellConfigurator).reuseId == "MoonCollectionViewCell" {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: type(of: cellConfigurator).reuseId, for: indexPath) as? MoonCollectionViewCell {
cell.contentView.layoutIfNeeded()
let size = cell.selfSizedTableView.intrinsicContentSize
cellHeight = size.height
}
}
return CGSize.init(width: cellWidth, height: cellHeight)
}
sizeForItem is called before cellForItem, that's the reason of the layoutIfNeeded, because I couldn't get the correct intrinsic content size.
I have removed the XIB as suggested, and designed my UICollectionViewCell within the Storyboard.
Here is my UICollectionViewCell designed within a Storyboard (only the UITableViewCell is designed in a XIB file)
I only added an UITableView within the UICollectionViewCell.
I want the UICollectionViewCell to adapt its size according to the height of the tableView.
Now here is my tableView :
I have created a subclass of UITableView (from this post)
class SelfSizedTableView: UITableView {
var maxHeight: CGFloat = UIScreen.main.bounds.size.height
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
self.layoutIfNeeded()
}
override var intrinsicContentSize: CGSize {
let height = min(contentSize.height, maxHeight)
return CGSize(width: contentSize.width, height: height)
}
}
Please note that I have disabled scrolling, I have dynamic prototype for the tableView cells, the style is grouped.
EDIT : Check the configure method, it comes from a protocol I used to configure in a generic way all my UICollectionViewCell
func configure(data: [MoonImages]) {
selfSizedTableView.register(UINib.init(nibName: "MoonTableViewCell", bundle: nil), forCellReuseIdentifier: "MoonTableViewCell")
selfSizedTableView.delegate = self
selfSizedTableView.dataSource = moonDataSource
var frame = CGRect.zero
frame.size.height = .leastNormalMagnitude
selfSizedTableView.tableHeaderView = UIView(frame: frame)
selfSizedTableView.tableFooterView = UIView(frame: frame)
selfSizedTableView.maxHeight = 240.0
selfSizedTableView.estimatedRowHeight = 40.0
selfSizedTableView.rowHeight = UITableView.automaticDimension
moonDataSource.data.addAndNotify(observer: self) { [weak self] in
self?.selfSizedTableView.reloadData()
}
moonDataSource.data.value = data
}
FYI the dataSource is a custom dataSource, with dynamic value (Generics) and the observer pattern, to reload the collection/tableView when the data is set.
I also have this warning when I launch the App.
[CollectionView] An attempt to update layout information was detected
while already in the process of computing the layout (i.e. reentrant
call). This will result in unexpected behaviour or a crash. This may
happen if a layout pass is triggered while calling out to a delegate.
Any hints or advice on how I should handle this ?
Because I am facing a strange behavior, it's like my sizeForItem use random values. The UICollectionViewCell height is not the same than my UITableView intrinsic content size height.
If I have 2 rows within my UITableView, the UICollectionView is not always equal at this size. I really don't know how to achieve this...
Should I invalideLayout?
Maybe it's not the answer you wanted, but here're my two cents. For your particular requirements, the better solution is moving away from UITableView, and use UIStackView or your custom container view.
Here's why:
UITableView is a subclass of UIScrollView, but since you've disabled its scrolling feature, you don't need a UIScrollView.
UITableView is mainly used to reuse cells, to improve performance and make code more structured. But since you're making it as large as its content size, none of your cells are reused, so features of UITableView is not taken any advantage of.
Thus, actually you don't need and you should not use either UITableView or UIScrollView inside the UICollectionViewCell for your requirements.
If you agree with above part, here're some learnings from our practices:
We always move most of the underlying views and code logics, mainly data assembling, into a UIView based custom view, instead of putting in UITableViewCell or UICollectionViewCell directly. Then add it to UITableViewCell or UICollectionViewCell's contentView and setup constraints. With this structure, we can reuse our custom view in more scenarios.
For requirements similar to yours, we'll create a factory class to create "rows" similar to how you create "cells" for your UITableView, add them into a vertical UIStackView, create constraints deciding UIStackView's width. Auto layout will take care of the rest things.
In your usage with UICollectionViewCell, to calculate the wanted height, inside preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) func of your cell, you can use contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) to calculate the height, do some check and return. Also, remember to invalidate layout when the width of the UICollectionView changes.
It is indeed very tricky, but I found a working way to solve this problem. As far as i know i got this from a chat app, where message bubble sizes are dynamic.
Here we go:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
// Minimum size
let frame = CGRect(x: 0, y: 0, width: view.frame.width - 30, height: 0)
let cell = MoonCollectionViewCell()
// Fill it with the content it will have in the actual cell,
// cell.content is just an example
let cell.content = items[indexPath.item]
cell.layoutIfNeeded()
// Define the maximum size it can be
let targetSize = CGSize(width: view.frame.width - 30, height: 240)
let estimatedSize = cell.systemLayoutSizeFittingSize(tagetSize)
return CGSize(width: view.frame.width - 30, height: estimatedSize.height)
}
What it basically do is, to define a minimum frame and the size that is targeted. Then by calling systemLayoutSizeFittingSize, it resizes the cell to the optimal size, but not larger than the targetSize.
Adjust the code to your needs, but this should work.
I tried to find the culprit in the posted code, but it seems that there are many moving parts. So, I will try to give some hints, that hopefully could help.
In theory (there is caveat for iOS 12), self sizing UICollectionViewCells should not be difficult. You essentially could set the collectionViewLayout.estimedItemSize to any value (preferred is the constant below), like this:
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
Then you have to make sure the constraints in the cells are set in a way that it can self size; that is auto layout can calculate the width and the height of the cell. You are providing an intrinsicContentSize of the tableView and it is wrapped by its super view from all four ends, so this should be OK.
Once you set the estimatedItemSize as shown above, you should not implement the delegate method returning the size:
func collectionView(_: UICollectionView, layout: UICollectionViewLayout, sizeForItemAt: IndexPath) -> CGSize
A quick tutorial can be found here for further reference: https://medium.com/#wasinwiwongsak/uicollectionview-with-autosizing-cell-using-autolayout-in-ios-9-10-84ab5cdf35a2
As I said in theory it should not be difficult, but cell auto sizing seems broken on iOS 12 see here In iOS 12, when does the UICollectionView layout cells, use autolayout in nib
If I were in you position, I would start from afresh, adding complexity step by step:
try implement the self sizing cells, possibly with with a simple UIView and an override of intrinsicContentSize; possibly by using iOS 11.4 SDK to exclude issues relevant to iOS 12 (the easiest way is to download latest Xcode 9 and work from there); if not possible do the iOS 12 fixes at this step
replace the simple view with a table view (which may also have dynamic sizing per see)
do the tableview reload data flow, i.e. dynamic sizing feature
if everything OK, do the iOS 12 fixes and migrate to iOS 12
Hope this helps.
BTW, the warning in the console is probably due to call to layoutIfNeeded() in the delegate method. It triggers an immediate layout pass, whereas this is done for the UICollectionView once all sizes are collected.
I need to make UICollectionView cells in oval shape where height is fixed but width is dynamic and it has a limit also, if text longer than that, then text should scroll. Any third party option available for this or need to create own using UICollectionView. Please guide.
Below is the image what i am trying to achieve. I want to know before starting should i look for third parties or use UICollectionView to make own. I have short time to complete that's why to avoid time on searching asking in starting itself which direction to follow.Please guide.
You can use a UICollectionViewFlowLayout and Auto Layout to achieve this.
Create a UICollectionViewCell with a container view.
Pin this container view the edges of the cell with auto layout
Add a UILabel to this container view and pin it to all edges of the container view (give it a background color to distinguish from the cell background)
In the UICollectionViewCell subclass you'll want to round the corners of the container view, e.g. self.containerView.layer.cornerRadius = self.containerView.height / 2
In the UICollectionViewFlowLayoutDelegate method, estimatedSizeForItem return an approximate size for the cell (auto layout will calculate the actual size.)
The important thing to remember is your cell needs to have enough constraints so that the auto layout engine can calculate the actual height and width based on the content.
Edit: If you want a fixed height, ensure your label can only have a single line. Or add a height constraint.
Finally, i found a library TagListView that can be installed through cocoapods with lots of customisation and swift 4 support also.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let nw = intersts[indexPath.row]
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let size = CGSize(width: 250, height: 1500)
let estimatedFrame = NSString(string: nw).boundingRect(with: size, options: options, attributes: [NSAttributedStringKey.font : UIFont.systemFont(ofSize: 17)], context: nil)
let attributes = [NSAttributedStringKey.font : UIFont.systemFont(ofSize: 17)]
let yourLabelSize: CGSize = nw.size(withAttributes:attributes )
var width1 = yourLabelSize.width + 30
if width1 < 30 {
width1 = 30
}
return CGSize(width: estimatedFrame.width+20, height: estimatedFrame.height+20)
}
I just extend or implement #Tim answer. So after you build the cell as described in his answer, then specify the cell width and height to be flexible using the sample code below i.e similar to what he described
let collectionViewLayout = UICollectionViewFlowLayout()
collectionViewLayout.scrollDirection = .horizontal
let itemWidth = Constants.myCollectionViewItemWidth
let itemHeight = Constants.myCollectionViewItemHeight
collectionViewLayout.estimatedItemSize = CGSize(width: itemWidth, height: itemHeight)
myCollectionView.collectionViewLayout = collectionViewLayout
In my app I am using using UICollectionView. I want to set fix space between UICollectionViewCell. so how can I do this?
here is my code:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return 20
}
by this line I can set space between cell?
here is my screenshot please see this. and let me know how can i set fix distance in both landscape or portrait mode
Maybe,you need implementation follow method.
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
You can adjust the spacing between UICollectionCell by using the storyboard Min. spacing property of UICollectionView.
Here you have to set Min spacing value for cells and lines.
Hope it would help you.
You can also manage the spacing using the size inspector of Collection View
http://i.stack.imgur.com/Nvp3g.png
If you want exact spacing between the cells you need to calculate the size for the cells that will best fit to allow for the spacing you need without wrapping while considering the sectionInsets and CollectionView bounds. eg.
let desiredSpacing: CGFloat = 20.0
if let layout = self.collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
layout.minimumLineSpacing = desiredSpacing
let totalCellsContentWidth = self.collectionView!.bounds.width - layout.sectionInset.left - layout.sectionInset.right
let numberOfCellsPerRow: CGFloat = 10
let numberOfSpacesPerRow = numberOfCellsPerRow - 1
let cellWidth = (totalCellsContentWidth - (numberOfSpacesPerRow * desiredSpacing) / numberOfCellsPerRow)
layout.itemSize = CGSize(width: cellWidth, height: cellWidth)
}
Assuming your cells are the same size the minimumLineSpacing is simple but would otherwise expand to fit the largest cell on the row before wrapping to the next line. As you can see it's the cell spacing that is a bit more complicated.
How am I supposed to set the height of the cells in UICollectionView equal to the height of whatever the collection view is? The code below doesn't work because the collection views height is not known at this point it seems since the auto layout is messing with the properties at this stage. It causes the cells height to be higher than the actual collection view.
I will add 300 in bounty to an answer that solves this!
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSize(width: 100, height: collectionView.frame.size.height)
}
2015-12-16 18:43:53.643 My-app[1055:434762] the behavior of the enter
code hereUICollectionViewFlowLayout is not defined because: 2015-12-16
18:43:53.643 My-app[1055:434762] 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. 2015-12-16
18:43:53.643 My-app[1055:434762] Please check the values return by the
delegate. 2015-12-16 18:43:53.644 My-app[1055:434762] The relevant
UICollectionViewFlowLayout instance is , and it is attached to ; layer = ; contentOffset: {0, 0}; contentSize: {3087, 307}>
collection view layout: .
2015-12-16 18:43:53.644 My-app[1055:434762] Make a symbolic breakpoint
at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the
debugger.
Solution that works based on hannads suggestion. If there are better ways please let me know
Made a property with a property observer.
var myCollectionViewHeight: CGFloat = 0.0 {
didSet {
if myCollectionViewHeight != oldValue {
myCollectionView.collectionViewLayout.invalidateLayout()
myCollectionView.collectionViewLayout.prepareLayout()
}
}
}
Override this method (it is called multiple times)
override func viewDidLayoutSubviews() {
myCollectionViewHeight = myCollectionView.bounds.size.height
}
Then I have this in my delegate:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSize(width: 100, height: myCollectionViewHeight)
}
Here is how I would do it. First add a global variable in the view controller which will be used to store the height of the collection view. In the viewDidLayoutSubviews method, set this variable to the height of the collectionView, and if it changed, call a method to invalidate the UICollectionView layout
collectionView.collectionViewLayout.invalidateLayout()
Which will call the method to set sizes of the collectionView cells, and in that method, set the height of the cell to the global variable holding the height of the collectionView.
Note: currently on my mobile and did not test this code. I might have missed something.
You try set this code to your viewDidLoad:
self.automaticallyAdjustsScrollViewInsets = false
Hope this help!
I had been struggling with this issue for over a week and finally found an answer that worked for my specific case. My issue was a result of 1) loading remote images in the UICollectionViewCell's image, and 2) having an estimated cell size that was larger than what I set manually on the sizeForItemAt of the collection view layout.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.bounds.width, height: collectionView.bounds.height)
}
I was able to solve by reducing the cell size on the storyboard to an amount lower than my cell's size, and I also set "Estimate Size" to "None" (this last part is probably enough)
In my case, I have to set collectionViewLayout.estimatedItemSize to an explicit value (which has its height <= collection view's height) instead of UICollectionViewFlowLayout.automaticSize.
The UICollectionViewFlowLayout.automaticSize results a default 50pt by 50pt cell size, which is higher than my collection view. In this case, UIKit refuses to do any layout work, despite that the actual cell size after self-sizing calculation will be smaller.
// horizontally scrollable collection view
private var viewHeight: CGFloat?
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if viewHeight != view.bounds.height {
viewHeight = view.bounds.height
layout.estimatedItemSize = CGSize(width: 50, height: viewHeight!)
layout.invalidateLayout()
}
}
I had this issue because I had pinned my UICollectionView to the bottom of a UITableViewCell without giving the collectionView a height.
After making a few network requests and then calling [self.tableView reloadData] in the UIViewController, I got the same error message in the log.
I managed to resolve it by breaking the bottom constraint and giving the UICollectionView a fixed height.
I ran into this issue as well and i was able to solve it by subtracting the relevant sectionInsets. Once you subtract the insets that aren't considered in the .size.height property it accepts the height.
Inside sizeForItemAtIndexPath you need to subtract the section inset dimensions...
let sectionInset = self.collectionView?.collectionViewLayout.sectionInset
let heightToSubtract = sectionInset!.top + sectionInset!.bottom
return CGSize(width: 100.0, height: (self.collectionView?.bounds.height)! - heightToSubtract)
NOTE: I am force unwrapping the optionals here but to be safe you might want to check them
In my case , I add collectionview to tableviewcell, and collectionview item height is equal to tableviewcell height. when change tableviewcellheiht, i get this warning.
to solve this , try this code
if (#available(iOS 11.0, *)) {
_collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
}
This problem can be easily solved by setting the collectionViewLayout itemSize property inside your viewdidload:
collectionViewLayout.itemSize = .init(width: 80, height: 80)
I solved this problem with this solution:
Cell size is equal collectionView size
2. Change in storyboard "Inset from:" from Safe Area to Content Inset
Change "Content Insets" to Never