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 have a UICollectionView with a cell only contain of one label. I've implemented it working fine, but some string values set to label inside cell cut out. Check below image.
Select cell should display as "Day Before Yesterday". If there's a way to adjust cell width base on data length I can fix this. is it possible ?
PS: some similar questions suggested below method. so I've tried it but no luck.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if(collectionView == dateRangeCollectionView){
return (dateRanges[indexPath.item] as NSString).size(attributes: nil)
}else{
return (OrderStatus[indexPath.item] as NSString).size(attributes: nil)
}
}
I don't think this method even exist in swift 4. when I start to type "sizeForItemAt" Xcode didn't suggest this method.
Please use Self Sizing Cells. Follow the below steps for more informaton
Self sizing cells are only supported with flow layout so make sure thats what you are using.
Follow the following steps :
Set estimatedItemSize on UICollectionViewFlowLayout
Add support for sizing on your cell subclass (Autolayout). More info here.
Checkout this github repo : SelfSizingCollectionViewCell
To calculate the size of each cell, you will need to figure out the width that the text would take, also accounting for the padding given. Use this to get the width of the cell, and return the value in the sizeForItem delegate.
func labelSizeWithString(text: String,fontSize: CGFloat, maxWidth : CGFloat) -> CGRect{
let font = UIFont.systemFontOfSize(fontSize)
let label = UILabel(frame: CGRectMake(0, 0, maxWidth, CGFloat.max))
label.numberOfLines = 0
label.font = font
label.text = text
label.sizeToFit()
return label.frame
}
If you want the text to appear in one or two lines, use the option for numberOfLines.
For the width you get, add in the appropriate padding value.
This question already has answers here:
Left Align Cells in UICollectionView
(24 answers)
Closed 5 years ago.
I have a UICollectionView like this:
These are the three different cases for it. Light grays shows the borders of the collection view and dark gray is for cells.
I have set min spacing and section insets to 0. But still I am getting these unwanted insets, and it seems it only happens when there are more than 1 cell.
I calculate the item sizes like this:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
guard let item = place?.publicTransports?[indexPath.row] else {
return CGSize.zero
}
var lines = ""
for (i, line) in item.lines.enumerate() {
lines.appendContentsOf(line)
if i != item.lines.count - 1 {
lines.appendContentsOf(", ")
}
}
let linesString = lines as NSString
return CGSize(width: linesString.sizeWithAttributes(nil).width + 35 + 20, height: collectionView.bounds.height/2)
}
Any suggestions?
It seems the question should read, left align a UICollectionView.
The simplest solution is to go to:
https://github.com/mokagio/UICollectionViewLeftAlignedLayout
And subclass your layout to this, this will ensure your UICollectionView left aligns, you should set all your insets to 0 first then begin changing them according to your design.
I have seen some questions similar to this one but none have really helped me. The last item in my collection view is always lower than the other items, as you can see in the image below:
If I increase the height of the UICollectionView then the last image alligns correctly but there is a huge gap between at the top and bottom of the UICollectionView as seen in the image below:
The sizing of the cells are controlled by this code:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let image = UIImage(data: imageArray[indexPath.row])
var size = image?.size
let height = size!.height
let width = size!.width
let imagewidth = (width / height) * 214
return CGSize(width: imagewidth, height: 214)
}
From what I understand, this should be making the height of each of the images the same, meaning it doesn't matter what the height of the original image is.
I also have another collection view in which the last item is aligned correctly and is set up in the exact same way so I'm a bit confused. What do you think the problem is? I have read that this could be a bug, but is there anyway around it? I am still learning about Swift and programming in general so if you have any other tips for me, they would be massively appreciated.
Thanks
If there is a huge gap between at the top and bottom of the UICollectionView , you can solve the problem via setting viewController's automaticallyAdjustsScrollViewInsets false.
override func viewDidLoad() {
super.viewDidLoad()
//remove the blank of top and bottom in collectionView
self.automaticallyAdjustsScrollViewInsets = false
}
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)
}