Swift UITableView can't calculate content height properly - ios

I'm having this weird issue with UITableView that can't calculate it's content's height properly.
I have custom UITableView class that is embedded in another custom UITableView, I want it to auto-adjust it's height to fit content so I have already:
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
And now when I use:
self.estimatedRowHeight = UITableView.automaticDimension // non-zero value like 40 isn't working either
self.rowHeight = UITableView.automaticDimension
the output is the frame that is not full height, when I turn "Scrolling enabled" in this TableView it's scrollable with full content (don't want that):
Now when I change
self.estimatedRowHeight = UITableView.automaticDimension
to:
self.estimatedRowHeight = 0
the output is exactly what I would want to have except the content text is cut...
Here's my CommentCell:
Console isn't showing any errors with autolayout in any case.
Do you maybe know what's going on? I have spent literally days trying to get those comments to work and that's the last thing I need.
If you need any more info please just tell me.
Edit:
If i change estimatedRowHeight to a large number for example 500 I get loads of empty space under cells:
So it looks like TableView can't fix the cell height to content. Maybe this will help someone.

Maybe it's about the textfield inside the CellView. Did you set it's Layout to wraps?
Also I would try to set it's intrinsic size value to 'placeholder' inside the Size Inspector.

Related

A mystery about iOS autolayout with table views and self-sizing table view cells

To help in following this question, I've put up a GitHub repository:
https://github.com/mattneub/SelfSizingCells/tree/master
The goal is to get self-sizing cells in a table view, based on a custom view that draws its own text rather than a UILabel. I can do it, but it involves a weird layout kludge and I don't understand why it is needed. Something seems to be wrong with the timing, but then I don't understand why the same problem doesn't occur for a UILabel.
To demonstrate, I've divided the example into three scenes.
Scene 1: UILabel
In the first scene, each cell contains a UILabel pinned to all four sides of the content view. We ask for self-sizing cells and we get them. Looks great.
Scene 2: StringDrawer
In the second scene, the UILabel has been replaced by a custom view called StringDrawer that draws its own text. It is pinned to all four sides of the content view, just like the label was. We ask for self-sizing cells, but how will we get them?
To solve the problem, I've given StringDrawer an intrinsicContentSize based on the string it is displaying. Basically, we measure the string and return the resulting size. In particular, the height will be the minimal height that this view needs to have in order to display the string in full at this view's current width, and the cell is to be sized to that.
class StringDrawer: UIView {
#NSCopying var attributedText = NSAttributedString() {
didSet {
self.setNeedsDisplay()
self.invalidateIntrinsicContentSize()
}
}
override func draw(_ rect: CGRect) {
self.attributedText.draw(with: rect, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin], context: nil)
}
override var intrinsicContentSize: CGSize {
let measuredSize = self.attributedText.boundingRect(
with: CGSize(width:self.bounds.width, height:10000),
options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin],
context: nil).size
return CGSize(width: UIView.noIntrinsicMetric, height: measuredSize.height.rounded(.up) + 5)
}
}
But something's wrong. In this scene, some of the initial cells have some extra white space at the bottom. Moreover, if you scroll those cells out of view and then back into view, they look correct. And all the other cells look fine. That proves that what I'm doing is correct, so why isn't it working for the initial cells?
Well, I've done some heavy logging, and I've discovered that at the time intrinsicContentSize is called initially for the visible cells, the StringDrawer does not yet correctly know its own final width, the width that it will have after autolayout. We are being called too soon. The width we are using is too narrow, so the height we are returning is too tall.
Scene 3: StringDrawer with workaround
In the third scene, I've added a workaround for the problem we discovered in the second scene. It works great! But it's horribly kludgy. Basically, in the view controller, I wait until the view hierarchy has been assembled, and then I force the table view to do another round of layout by calling beginUpdates and endUpdates.
var didInitialLayout = false
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if !didInitialLayout {
didInitialLayout = true
UIView.performWithoutAnimation {
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
}
}
The Mystery
Okay, so here are my questions:
(1) Is there a better, less kludgy workaround?
(2) Why do we need this workaround at all? In particular, why do we have this problem with my StringDrawer but not with a UILabel? Clearly, a UIlabel does know its own width early enough for it to give its own content size correctly on the first pass when it is interrogated by the layout system. Why is my StringDrawer different from that? Why does it need this extra layout pass?

How to dynamically size UIScrollView?

I'm adding content to the UIScrollView dynamically. The content size of the scroll view is increasing. How can I change the content size to fit the dynamically added content? See the code I'm using that is not working
extension UIScrollView {
func updateContentView() {
contentSize.height = subviews.sorted(by: { $0.frame.maxY < $1.frame.maxY }).last?.frame.maxY ?? contentSize.height
contentSize.height += 300
}
}
refer this question
I hope the above solves your problem but I'd prefer using a UITableView or a UICollectionView in place of this because everything is handled so smoothly in them. You just have to give the number of rows(return a variable which changes dynamically). That's it, your contentSize is adjusted internally.
But again if you have a different requirement please go with the reference link!

TableView resizing parent view iOS

This is a problem that has been bugging me for quite some time.
Assume a view that holds a tableView with X items. The goal is to make that view resize so that it is as high as the contents of the tableView.
An approach
Calculate the contents of the tableView in total ( e.g if there are 5 rows and each is 50 units high, its just a multiplication matter ). Then set the tableView constrained at a 0 0 0 0 into the view and set the view height to 250.
This works well for fixed height cell sizes. However!
a) How would the problem be approached for dynamic height cells though with complex constraints in a scenario where resizing happens automatically and the tableHeightForRow is set to UITableViewAutomaticDimension?
b) An idea could be using tableView.contentSize. However when would we retrieve that value safely in order to set the parent view frame accordingly? Is that even possible?
Thanks everyone
If you have a UITableView subclass, you can set up a property observer on the contentSize like this:
override var contentSize: CGSize {
didSet {
// make delegate call or use some other mechanism to communicate size change to parent
}
}
The most straightforward approach to this in my opinion is to use Autolayout. If you take this approach, you can use the contentSize to automatically invalidate the intrinsicContentSize which is what autolayout uses to dynamically size elements (as long as they don't have higher priority placement constraints restricting or explicitly setting their size).
Something like this:
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
return contentSize
}
Then, just add your table view to your parent view hierarchy with valid placement constraints and a content hugging/compression resistance of required.

Auto height UICollectionView

I am using UICollectionView and in it my cells have auto width based on the content(text size) e.g. first row might contain 8 items and 2nd row might contains only 1. This is working fine.
I want to set the height of my UICollectionView to show all the available items but not more(not the empty space at the bottom...). If I use auto layout than I have to set the height or bottom align constraint.
But in this way the height will be fixed. Is there any way I can get number of rows in and calculate height dynamically?
Here is what my story board look like:
You could use fixed height constraint at design time and than change it at runtime when deferred height value calculated. Height constraint need to be referenced in code and than needs to be changed with your calculated height.
// Height constraint of collection view
#IBOutlet weak var heightContstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
setHeightOfCollectionView()
}
func setHeightOfCollectionView() {
// Calculate height of collection view depending on collection item count
let calculatedHeight:CGFloat = 500.0
heightContstraint.constant = calculatedHeight
}
You should check the images below that demonstrate how to do.
Hope it would help.

UITableViewCell + Dynamic Height + Auto Layout

I came from this great answer: Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
I've implemented the things described in that answer but I'm facing with a little different scenario. I haven't one UILabel but instead I have a dynamic list of UILabels.
I've created an image showing some different cases of what the table view should look:
At the current state of the repo the cell doesn't grow vertically to fit the cell's contentView.
UPDATE
REPO: https://github.com/socksz/DynamicHeightCellAutoLayout
If you try to get the project from the repo and run it, you can see exactly what is the problem I'm referring. I can't get what is missing for let it works.
The problem here is with the third party component you are using, FXLabel, not with any of the code around table views or Auto Layout in them. In order to support Auto Layout, custom subclasses of UIView must implement the -[intrinsicContentSize] method appropriately, and then call -[invalidateIntrinsicContentSize] when something changes it.
In this case, FXLabel appears to be relying on its superclass implementation (UILabel) for the above methods, and since UILabel was not designed to handle variable line spacing in the way that FXLabel implements it, it doesn't know the correct intrinsicContentSize to return, and therefore the Auto Layout calculations are wrong (in this case, since the intrinsic content size is too small). Check out the "Enabling Custom Views for Auto Layout" section of this excellent obcj.io article for more details.
Now the good news is that as of iOS 6, you should be able to accomplish this using an attributed string in a standard UILabel. Check out the Stack Overflow answer here.
If for some reason you really like FXLabel, perhaps you could open an issue on the GitHub project (or try and fix it yourself and submit a pull request).
To set automatic dimension for row height & estimated row height, ensure following steps to make, auto dimension effective for cell/row height layout.
Assign and implement dataSource and delegate
Assign UITableViewAutomaticDimension to rowHeight & estimatedRowHeight
Implement delegate/dataSource methods (i.e. heightForRowAt and return a value UITableViewAutomaticDimension to it)
-
#IBOutlet weak var table: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Don't forget to set dataSource and delegate for table
table.dataSource = self
table.delegate = self
// Set automatic dimensions for row height
// Swift 4.2 onwards
table.rowHeight = UITableView.automaticDimension
table.estimatedRowHeight = UITableView.automaticDimension
// Swift 4.1 and below
table.rowHeight = UITableViewAutomaticDimension
table.estimatedRowHeight = UITableViewAutomaticDimension
}
// UITableViewAutomaticDimension calculates height of label contents/text
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// Swift 4.2 onwards
return UITableView.automaticDimension
// Swift 4.1 and below
return UITableViewAutomaticDimension
}
For label instance in UITableviewCell
Set number of lines = 0 (& line break mode = truncate tail)
Set all constraints (top, bottom, right left) with respect to its superview/ cell container.
Optional: Set minimum height for label, if you want minimum vertical area covered by label, even if there is no data.

Resources