iOS Auto-Layout: Dynamic height for table view cell - ios

I have a table view with a bunch of cells (custom cell, which only has its content view).
In tableView:cellForRowAtIndexPath: I'm adding a predefined UIView (which has several subviews) to the content view of the custom cell. I set up all constraints for the UIView and its subviews before.
Last but not least, I set the vertical and horizontal constraints for the content view of my custom cell (superview) and the UIView, which was added before (subview).
The constraint strings look like this:
H:|[view]|
V:|[view]|
Unfortunately, I still get the default height for all table view cells. I'm wondering If there's a way to let auto layout do the calculation of the height automatically according to content size.

Check out my detailed answer to this question here: https://stackoverflow.com/a/18746930/796419
It takes a bit of work to set up, but you can absolutely have Auto Layout constraints driving a completely dynamic table view without a single hardcoded height (and let the constraint solver do the heavy lifting and provide you with the row height).

Auto Layout won't help with the cell height. You'll need to set that in tableView:heightForRowAtIndexPath. I guess you're probably asking this because your cell heights are variable, not fixed. i.e., they depend on the content.
To resolve that, pre-calculate the cell heights and store them in an array. Return the value for the appropriate indexPath in the tableView:heightForRowAtIndexPath method.
Be sure to calculate content sizes on the main thread, using sizeThatFits of UILabel classes and such like.
If your calculation is intensive, do the majority of it off main apart from the view related methods such as sizeThatFits.

I solved the problem by using CGSize size = [view systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; in tableView:heightForRowAtIndexPath:.

To set automatic dimensions for 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)
Swift:
#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.

Related

UITableView Alignment with UITextView

I am trying to make a UITableView line up with the height sizing of paragraphs in a UITextView. Example: The timestamps to the left are what I am trying to do. I changed my code to use UIView's instead of TVcells to see what was wrong and you can see the orange view is overlapping the cyan one, meaning that the views don't actually line up but they overlap. NOTE: I am wanting to use the TableView not UIView's I am having trouble understanding how the text heights are calculated in iOS. I am using the below code to get the heights of each paragraph:
let liveParagraphView = textView.selectionRects(for: txtRange).reduce(CGRect.null) { $0.union($1.rect) }
After this I calculate the height of each then feed that into my UITableView heightForRowAt
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let models = getParagraphModel()
let height = models[indexPath.row].height
let finalHeight = models[indexPath.row].height
let heightValue = finalHeight
return CGFloat(heightValue);
}
Every line has different height values but even when using these values it's not lining up. The problem seems to be that every line calculates a Y Position which is not directly under the line before it. It's ON TOP OF!! Resulting in the UITableView not being alined when new cells are added and that 'overlay' of the selectionRects isn't taken into account. Am I correct by this? How could I go about achieving this?
Swift 5
Firstly you should set your textView (which is in the cell) dynamic height:
textView.translatesAutoresizingMaskIntoConstraints = true
textView.sizeToFit()
textView.isScrollEnabled = false
Then calculate your textView's number of lines in textDidChange etc. for update tableView's layout.
let numOfLines = (yourTextView.contentSize.height / yourTextView.font.lineHeight) as? Int
When textView's text one line down you should update tableView layout:
tableView.beginUpdates()
tableView.endUpdates()
And then you should set your tableView cell's intrinsicContentSize for dynamic rowHeight:
Set your cell's (which is the contains textView) layout without static height,
Set your tableView's rowHeight and estimatedRowHeight:
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 44 // whatever you want
So now you have tableView cell with dynamicHeight

Pin cell height to UICollectionView height

I have a horizontal UICollectionView that's contained inside a UICollectionView header of a vertical UICollectionView. The header of the vertical UICollectionView has a dynamic height that changes due to user interaction (dragging the outer UICollectionView down expands it, scrolling up collapses it). The horizontal UICollectionView is constrained to the size of the header and will also shrink in height when the header is reduced in height. When this happens I get this error
the behavior of the UICollectionViewFlowLayout is not defined because:
2018-07-19 13:42:09.959 IDAGIO[81891:2239798] 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.
2018-07-19 13:42:09.959 IDAGIO[81891:2239798] Please check the values
return by the delegate.
because the UICollectionView shrinks in height, but the cells of it keep their old size and are therefore higher than allowed (I only get this error while scrolling up, so it's not related to any insets, the heights are fine, they just don't match exactly at that point in time).
I already tried to call collectionView.collectionViewLayout.invalidateLayout() every time I get a scroll event of the vertical collection view and then return the collection view size in
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
but the layout update then is always a bit too late, so I still get the above error. Also it looks weird as the cell height also visually is a bit behind.
My question: is there a way to dynamically respond to height changes of a horizontal UICollectionView so the cell height is updated accordingly and in time? (Maybe setup some height constraint that automatically keeps the cell height equal to the collection view height?)
I know about dynamic cell sizing, but that usually means to constrain a cell to the size of it's content. What I want to achieve is to always constrain the height of the cell (and it's content) to the collection view height.
I found an easy solution myself, without having to pass or store the scroll offset at all:
Just subclass UICollectionViewFlowLayout, override shouldInvalidateLayout and set the itemSize to the size of the new bounds.
override func shouldInvalidateLayout(forBoundsChange: CGRect) -> Bool {
if !forBoundsChange.size.equalTo(collectionView!.bounds.size) {
itemSize = forBoundsChange.size
return true
}
return false
}
In Swift 4.2
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if !newBounds.size.equalTo(collectionView!.bounds.size) {
itemSize = newBounds.size
return true
}
return false
}
UICollectionView has a property derived from UIScrollView called contentinsetadjustmentbehavior, setting that to false should fix your issue if you are setting the size of the items on the collectionView to the size of the bounds of the collectionView.
Because the scrollView can be adjusting the insets which in turns changes the collectionView.adjustedContentInset
https://developer.apple.com/documentation/uikit/uiscrollview/2902261-contentinsetadjustmentbehavior
So basically setting:
collectionView.contentInsetAdjustmentBehavior = .never
Reloading data on scroll event is one of the options but it is definitely not the best one because of all the calculations involved in reloading data again and again. You'll probably find a better solution by subclassing UICollectionViewLayout and do all the calculations on prepare(). Then you can just use delegation to pass the scroll offset to your custom layout and make the cell sizing accordingly.

How to make a self sizing basic UITableViewCell?

I know how to make a custom self sizing cell. But for some reason I'm facing challenges when trying to make a default one multi-line.
What I currently want is a cell which only has one label. So the default one with a built-in style "Basic" seems to be the best solution for something as simple as that. However it only shows 2 lines of text.
My current set-up: a static UITableView and a UITableViewController containing outlets to some of the cells that need to be configured.
Things I tried:
set number of lines to 0
set table view's row height to UITableViewAutomaticDimension
override heightForRowAtIndexPath so that it always returns UITableViewAutomaticDimension
call sizeToFit, setNeedsLayout, layoutIfNeeded on the cell and/or content view and/or text label
set custom cell height to 0 in storyboard
increase vertical and horizontal content hugging priorities for the label
EDIT:
I guess I wasn't really clear about what exactly is the problem. I'm not using a custom cell. I'm trying to get away with the basic one.
This means you can't add any constraints to its label. Sure, you can programmatically but since everything is managed internally for Apple's built-in styles it may result in a conflict.
Additional details:
At this point (as I mentioned above) I have a UITableViewController with outlets to specific cells: #IBOutlet weak var descriptionCell: UITableViewCell!
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
Is it really possible? Since I already spent too much time trying to avoid making a custom cell I'll finally go make it. Anyway any solution is welcome.
It's no need to do following two.
call sizeToFit, setNeedsLayout, layoutIfNeeded on the cell and/or content view and/or text label
set custom cell height to 0 in storyboard
And you should check you label's constraints. For example, it should has fixed width at run time and has constraints with cell's top and bottom. So the cell will grow itself.
Try this approach:
Set number of lines to 0
Set table view's row height to UITableViewAutomaticDimension
Override heightForRowAtIndexPath so that it always returns UITableViewAutomaticDimension
NsLayConstraints for label:
top = cell.top
bottom = cell.bottom
leading = cell.leading.padding
trailing = cell.leading.padding (Padding is optional)
It works for me. All you need to do in your code is
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 44.0 // or whatever height is closest to what your cells will be
tableView.rowHeight = UITableViewAutomaticDimension
}
Everything you listed after "set table view's row height to UITableViewAutomaticDimension" is not necessary. Just set number of lines in your cell's label to 0 like you did and include the code above.

Custom UIView in UITableView cells with UITableViewAutomaticDimension enabled

I have UITableView with UITableViewAutomaticDimension and some estimatedRowHeight. For this table I am using custom UITableViewCell which contains some label and custom UIView with overridden intrinsicContentSize(). Constraints setup is correct and table is able to determine actual height for each row. So far so good.
Now I started to modify internal logic of my custom view to adapt it's appearance based on available width i.e. when table cell size is not wide enough my view can rearrange subviews to fit new limitation and this have impact to resulting height, so I have code like that:
var internalSize: CGSize = ...
override func intrinsicContentSize() -> CGSize {
return internalSize
}
override func layoutSubviews() {
super.layoutSubviews()
fitIntoWidth(frame.size.width)
}
private func fitIntoWidth(width: CGFloat) {
let height = // calculate based on content and width
internalSize = CGSizeMake(width, height)
invalidateIntrinsicContentSize()
}
Now, when I populate table view, intrinsicContentSize() returns some desired value but it is not good fit for current layout, then control goes to layoutSubviews() where size get recalculated and system again calls intrinsicContentSize() and now it returns good value. However, first time table loads data and cell heights calculated based on incorrect intrinsicContentSize() values. If I call reloadData() again all becomes fine and layout is also ok for all upcoming cells in table.
Where is my mistake and how to modify code to make cell sizing work correctly without calling reloadData() twice?

Dynamic height TableView in a Scroll View

I am designing a page having a scroll view and above it a table view(scroll disabled). For doing this I have referred answers in this question - Make UITableView not scrollable and adjust height to accommodate all cells ,but wasn't successful.
Hierarchy of views along with provided constraints-
-Main View
-Scroll view
pinned to all sides of main view(0,0,0,0), constraint to margins
-Content View
pinned to scroll view(0,0,0,0),equal width to main view,equal height to main view(priority - 250)
-Table view inside content view
scroll disabled,having 50 point spaces from all sides,Height(>=),bottom spacing 50(relation >=).I have put greater than equal so as to increase height dynamically.
Now when I populate my table view I use the code as
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableview.dequeueReusableCellWithIdentifier("cellreuse", forIndexPath: indexPath)
cell.textLabel?.text = name[indexPath.row]
tableview.frame.size = tableview.contentSize
return cell
}
So when I run my code, it increases the tableview frame but doesn't stretch the content size and it just becomes weird as my scroll view doesn't scroll to the end of the table view neither my table view obeys the auto layout constraints.
Just I needed to do this -
remove the line - tableView.frame.size = tableView.contentSize
Add a height constraint for table view.
Set priority to High
Create an outlet of the height constraint(Ctrl+Drag).
Wherever you need to reload data of your table, set the height constraint to tableview's content height.
tableHeightConstraint.constant = tableview.contentSize.height
Assign a table height. Let it be constant 0.
Just add below lines.
tableView.heightConstant.constant = CGFloat.greatestFiniteMagnitude
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.heightConstant.constant = tableView.contentSize.height
With this, you can easily achieve dynamic table height. Working on iOS 13, Swift 5.
Had the same issue and resolved it by doing the following:
Create an outlet of the height constraint for the table view with a priority of 1000
#IBOutlet private weak var tableViewHeight: NSLayoutConstraint!
On viewDidLayoutSubview call layoutIfNeeded on the table view and then set the table view height constraint to the height of the content view
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.layoutIfNeeded()
tableViewHeight.constant = tableView.contentSize.height
}
Tested on iOS 14.1 and iOS 16.1

Resources