UITableView + AutoLayout + Intrinsic Size: Where/when to calculate size? - ios

I have create a custom view MyIntrincView which calculates its height automatically when setting its content. This works fine both in simulator and InterfaceBuilder.
However, when placing MyIntrinsicView inside a UITableViewCell, the cell height is not calculated correctly. Instead of automatically adopting the cell height to the intrinsic height of the view, all cell keep the same, initial height.
// A simple, custom view with intrinsic height. The height depends on
// the value of someProperty. When the property is changed setNeedsLayout
// is set and the height changes automatically.
#IBDesignable class MyIntrinsicView: UIView {
#IBInspectable public var someProperty: Int = 5 {
didSet { setNeedsLayout() }
}
override func layoutSubviews() {
super.layoutSubviews()
calcContent()
}
func calcContent() {
height = CGFloat(20 * someProperty)
invalidateIntrinsicContentSize()
}
#IBInspectable var height: CGFloat = 50
override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: height)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
invalidateIntrinsicContentSize()
}
}
// A simple cell which only contains a MyIntrinsicView subview. The view
// is attached to trailing, leading, top and bottom anchors of the cell.
// Thus the cell height should automatically match the height of the
// MyIntrinsicView
class MyIntrinsicCell: UITableViewCell {
#IBOutlet private var myIntrinsicView: MyIntrinsicView!
var someProperty: Int {
get { return myIntrinsicView.someProperty }
set {
myIntrinsicView.someProperty = newValue
// Cell DOES NOT rezise without manualle calling layoutSubviews()
myIntrinsicView.layoutSubviews()
}
}
}
...
// Simple tableView delegate which should create cells of different heights
// by giving each cell a different someProperty
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "IntrinsicCell", for: indexPath) as? MyIntrinsicCell ?? MyIntrinsicCell()
// Give all cell a different intrinsic height by setting someProperty to rowIndex
cell.someProperty = indexPath.row
return cell
}
I would expect, that each cell has a different height (20 * someProperty = 20 * indexPath.row). However, instead all cell have the same, initial height.
Only when explicitly calling myIntrinsicView.layoutSubviews() the cells are created with the correct height.
It seems that the tableView does not call myIntrinsicView.layoutSubviews(). Why is this?
When using a UILabel instad of MyIntrinsicView as cell content with different text lengths, everything works as expected. Thus the overall tableView setup is correct (= cell sizes are calculated automatically) and there has to be way to use intrinsic sizes correctly in UITableView as well. So, what exactly is the correct way to do this?

As with your previous question here I think you're not really understanding what intrinsicContentSize does and does not do.
When setting the text of a UILabel, yes, its intrinsicContentSize changes, but that's not all that happens.
You also don't want to do what you're trying inside layoutSubviews() ... if your code does trigger a change, you'll get into an infinite recursion loop (again, as with your previous question).
Take a look at modifications to your code:
// A simple, custom view with intrinsic height. The height depends on
// the value of someProperty. When the property is changed setNeedsLayout
// is set and the height changes automatically.
#IBDesignable class MyIntrinsicView: UIView {
#IBInspectable public var someProperty: Int = 5 {
didSet {
calcContent()
setNeedsLayout()
}
}
func calcContent() {
height = CGFloat(20 * someProperty)
invalidateIntrinsicContentSize()
}
#IBInspectable var height: CGFloat = 50
override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: height)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
invalidateIntrinsicContentSize()
}
}
// A simple cell which only contains a MyIntrinsicView subview. The view
// is attached to trailing, leading, top and bottom anchors of the cell.
// Thus the cell height should automatically match the height of the
// MyIntrinsicView
class MyIntrinsicCell: UITableViewCell {
#IBOutlet private var myIntrinsicView: MyIntrinsicView!
var someProperty: Int {
get { return myIntrinsicView.someProperty }
set {
myIntrinsicView.someProperty = newValue
}
}
}
Two side notes...
1 - Your posted code shows you calling myIntrinsicView.layoutSubviews()... from Apple's docs:
You should not call this method directly. If you want to force a layout update, call the setNeedsLayout method instead to do so prior to the next drawing update. If you want to update the layout of your views immediately, call the layoutIfNeeded method.
2 - For the direction it looks like you're headed, you would probably be better off manipulating constraint constants, rather than intrinsic content size.

Related

UICollectionView height is not correct

I want to embed a UICollectionView inside a UIScrollView (I know it's not good practice but I have my reasons to do that).
I created a UICollectionView subclass that set intrinsicContentSize based on content size
class EmebedableCollectionView: UICollectionView {
override var intrinsicContentSize: CGSize {
let size = self.collectionViewLayout.collectionViewContentSize
let result = CGSize(width: max(size.width,1), height: max(size.height + 20,1))
self.setNeedsLayout()
return result
}
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
}
then in a container view, I create a height constraints for the collection view
#IBOutlet weak var productsHeightConstraint: NSLayoutConstraint!
and update it based on UICollectionView intrinsicContentSize
override func viewWillLayoutSubviews() {
for subview in productsContainerView.subviews.first!.subviews where subview is UICollectionView {
productsHeightConstraint.constant = subview.intrinsicContentSize.height
productsContainerView.layoutIfNeeded()
}
}
The problem is: the container view height is not correct, but when I push a new view controller and get back, it update the height to be correct. so what's wrong in my code?

Nest UICollectionView into UITableViewCell

I'm trying to build something
I'm trying to build a tag list view using UICollectionView and nest it into my custom UITableViewCell.
What do I have now
After searching the internet, I find the key to the problem:
Subclass UICollectionView and implement it's intrinsic content size property.
However, when I nest my custom UICollectionView into a self-sizing UITableViewCell, the whole thing doesn't work well. The layout is broken.
No matter how do I change the code, I get one of the following 3 buggy UIs.
The height of the collection view is always wrong, either too small or too large, it can not hug it's content just right.
When I use Debug View Hierarchy to check the views, I find that although the UI is broken, the contentSize property of the collection view has a correct value. It seems that the content size property can not be reflected to the UI in time.
class IntrinsicCollectionView: UICollectionView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: collectionViewLayout.collectionViewContentSize.height)
}
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
isScrollEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
There are many solution about how to create a custom UICollectionView with intrinsic content size. Some of them can work correctly. But when nesting them into a UITableViewCell, none of them works well.
There are also some answer about just nest one UICollectionView into UITableViewCell without other views. But if there are also some UILabel in UITableViewCell, it won't work.
I upload all the code to github. https://github.com/yunhao/nest-collectionview-in-tableviewcell
Thank you!
I'll try to explain what's going on....
To make it easy to understand, in your ListViewController let's work with just one row to begin with:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1 // items.count
}
In your ListViewCell class, add these lines at the end of prepareViews():
// so we can see the element frames
titleLabel.backgroundColor = .green
subtitleLabel.backgroundColor = .cyan
collectionView.backgroundColor = .yellow
In your IntrinsicCollectionView class, let's add a print() statement to give us some information:
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
// add this line
print("collView Width:", frame.width, "intrinsic height:", collectionViewLayout.collectionViewContentSize.height)
return CGSize(width: UIView.noIntrinsicMetric, height: collectionViewLayout.collectionViewContentSize.height)
}
When I then run the app on an iPhone 8, I get this result:
and I see this in the debug console:
collView Width: 66.0 intrinsic height: 350.0
collView Width: 343.0 intrinsic height: 30.0
What that tells me is that the collection view is asked for its intrinsicContentSize before it has a complete frame.
At that point, it fills in its cells, and its layout ends up with a .collectionViewContentSize.height of 350 (this row has six "tags" cells).
Auto-layout then performs another pass... the collection view now has a valid frame width (based on the cell width)... and the cells are re-laid-out.
Unfortunately, the table view has already set the row height(s), based on the initial collection view intrinsicContentSize.height.
So, two steps that may (should) fix this:
In ListViewCell, invalidate the content size of the collection view when you get the tags:
func setTags(_ tags: [String]) {
self.tags = tags
collectionView.reloadData()
// add this line
collectionView.invalidateIntrinsicContentSize()
}
Then, in ListViewController, we need to reload the table after its frame has changed:
// add this var
var currentWidth: CGFloat = 0
// implement viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if view.frame.width != currentWidth {
currentWidth = view.frame.width
tableView.reloadData()
}
}
That seems (with very quick testing) to give me reliable results:
and on device rotation:

How to adjust height of UICollectionView to be the height of the content size of the UICollectionView?

I would like the UICollectionView (The red one) to shrink to the height of the content size in this case UICollectionViewCells(the yellow ones) because there is a lot of empty space. What I tried is to use:
override func layoutSubviews() {
super.layoutSubviews()
if !__CGSizeEqualToSize(bounds.size, self.intrinsicContentSize) {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
return self.collection.contentSize
}
but return self.collection.contentSize always return (width, 0)
and for this reason it shrinks too much to value of height 30 (The value which I set in the XIB file for the height, although I have constaint >= 30).
I would suggest the following:
Add a height constraint to your collection view.
Set its priority to 999.
Set its constant to any value that makes it reasonably visible on the storyboard.
Change the bottom equal constraint of the collection view to greater or equal.
Connect the height constraint to an outlet.
Every time you reload the data on the collection view do the following:
You may also want to consider the Inset of the collection view by adding it to the content size.
Code Sample:
CGFloat height = myCollectionView.collectionViewLayout.collectionViewContentSize.height
heightConstraint.constant = height
self.view.setNeedsLayout() Or self.view.layoutIfNeeded()
Explanation: Extra, You don't have to read if you understand it. obviously!!
The UI will try to reflect all the constraints no matter what are their priorities. Since there is a height constraint with lower priority of (999), and a bottom constraint of type greater or equal. whenever, the height constraint constant is set to a value less than the parent view height the collection view will be equal to the given height, achieving both constraints.
But, when the height constraint constant set to a value more than the parent view height both constraints can't be achieved. Therefore, only the constraint with the higher priority will be achieved which is the greater or equal bottom constraint.
The following is just a guess from an experience. So, it achieves one constrant. But, it also tries to make the error in the resulted UI for the other un-achieved lower priority constraint as lowest as possible. Therefore, the collection view height will be equal to the parent view size.
In Swift 5 and Xcode 10.2.1
My CollectionView name is myCollectionView
Fix height for your CollectionView
Create Outlet for your CollectionViewHeight
IBOutlet weak var myCollectionViewHeight: NSLayoutConstraint!
Use below code
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let height = myCollectionView.collectionViewLayout.collectionViewContentSize.height
myCollectionViewHeight.constant = height
self.view.layoutIfNeeded()
}
Dynamic width for cell based on text content...
Dynamic cell width of UICollectionView depending on label width
1) Set Fix Height of your CollectionView.
2) Create Outlet of this CollectionView Height Constant.
Like :
IBOutlet NSLayoutConstraint *constHeight;
3) Add below method in your .m file:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGFloat height = collectionMenu.collectionViewLayout.collectionViewContentSize.height;
constHeight.constant = height;
}
I ended up, by subclassing the UICollectionView and overriding some methods as follows.
Returning self.collectionViewLayout.collectionViewContentSize for intrinsicContentSize makes sure, to always have the correct size
Then just call it whenever it might change (like on reloadData)
Code:
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
return self.collectionViewLayout.collectionViewContentSize
}
But be aware, that you lose "cell re-using", if you display large sets of data, eventhough they don't fit on the screen.
This seemed like the simplest solution for me.
class SelfSizingCollectionView: UICollectionView {
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
isScrollEnabled = false
}
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
return contentSize
}
}
You may not need to override reloadData
You have to set height constraint as equal to content size
HeightConstraint.constant = collection.contentSize.height
Took the solution by d4Rk which is great, except in my case it would keep cutting off the bottom of my collection view (too short). I figured out this was because intrinsic content size was sometimes 0 and this would throw off the calculations. IDK. All I know is this fixed it.
import UIKit
class SelfSizedCollectionView: UICollectionView {
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
let s = self.collectionViewLayout.collectionViewContentSize
return CGSize(width: max(s.width, 1), height: max(s.height,1))
}
}
Subclass UICollectionView as follows
Delete height constraint if any
Turn on Intrinsic Size
-
class ContentSizedCollectionView: UICollectionView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: collectionViewLayout.collectionViewContentSize.height)
}
}
If you set the height constraint of the collection view. Just observe the contentSize change in the viewDidLoad and update the constraint.
self.contentSizeObservation = collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, change in
guard let `self` = self else { return }
guard self.collectionView.contentSize != .zero else { return }
self.collectionViewHeightLayoutConstraint.constant = self.collectionView.contentSize.height
}
I have a multi-line, multi-selection UICollectionView subclass where the cells are of fixed height and left-aligned flowing from left to right. It's embedded in a vertical stack view that's inside a vertical scroll view. See the UI component below the label "Property Types".
In order for the collection view to fit the height of its contentSize, here's what I had to do (note that this is all within the UICollectionView subclass):
Give the collection view a non-zero minimum height constraint of priority 999. Auto-sizing the collection view to its content height simply won't work with zero height.
let minimumHeight = heightAnchor.constraint(greaterThanOrEqualToConstant: 1)
minimumHeight.priority = UILayoutPriority(999)
minimumHeight.isActive = true
Set the collection view's content hugging priority to .required for the vertical axis.
setContentHuggingPriority(.required, for: .vertical)
Calling reloadData() is followed by the following calls:
invalidateIntrinsicContentSize()
setNeedsLayout()
layoutIfNeeded()
For example, I have a setItems() function in my subclass:
func setItems(_ items: [Item]) {
self.items = items
selectedIndices = []
reloadData()
invalidateIntrinsicContentSize()
setNeedsLayout()
layoutIfNeeded()
}
Override contentSize and intrinsicContentSize as follows:
override var intrinsicContentSize: CGSize {
return contentSize
}
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
setNeedsLayout()
layoutIfNeeded()
}
}
Do following.
first set height constrain for UICollectionView
here calendarBaseViewHeight is UICollectionView height Variable
call the function after reload the collection view
func resizeCollectionViewSize(){
calendarBaseViewHeight.constant = collectionView.contentSize.height
}
first of all calculate number of cells than multiply it with height of cell and then return height in this method
collectionView.frame = CGRectMake (x,y,w,collectionView.collectionViewLayout.collectionViewContentSize.height); //objective c
//[collectionView reloadData];
collectionView.frame = CGRect(x: 0, y: 0, width: width, height: collectionView.collectionViewLayout.collectionViewContentSize.height) // swift
On your UICollectionView set your constraints such as Trailing, Leading, and Bottom:
If you look at my height constraint in more detail, as it is purely for storyboard look so I don't get errors, I have it to Remove at build time. The real height constraint is set in my code down below.
My code for DrawerCollectionView, which is set as the collection view Custom Class:
import UIKit
class DrawerCollectionView: UICollectionView {
override func didMoveToSuperview() {
super.didMoveToSuperview()
heightAnchor.constraint(equalToConstant: contentSize.height).isActive = true
}
}
Adjusting height of UICollectionView to the height of it's content size 🙌🏻
SWIFT 5
final class MyViewController: UIViewController {
// it's important to declare layout as separate constant due to late update in viewDidLayoutSubviews()
private let layout = UICollectionViewFlowLayout()
private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
setupCollectionViewConstraints()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateFlowLayout()
}
private func setupCollectionView() {
view.addSubview(collectionView)
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "UICollectionViewCell")
collectionView.dataSource = self
}
private func setupCollectionViewConstraints() {
// your collectionView constraints setup
}
private func updateFlowLayout() {
let height = collectionView.collectionViewLayout.collectionViewContentSize.height
layout.itemSize = CGSize(width: view.frame.width, height: height)
layout.scrollDirection = .horizontal
layout.minimumInteritemSpacing = .zero
layout.minimumLineSpacing = .zero
layout.sectionInset = UIEdgeInsets.zero
}
}
extension MyViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {...}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {...}
}
work for me
let heightRes = resCollectionView.collectionViewLayout.collectionViewContentSize.height
foodHeightConstrant.constant = height.advanced(by: 1 )
foodCollectionView.setNeedsLayout()
foodCollectionView.layoutIfNeeded()
I was using a UICollectionView in UITableView cell. For me, the following solution worked.
In parent view of collection view, I updated the height constraint in layoutSubviews method like this
override func layoutSubviews() {
heightConstraint.constant = myCollectionView.collectionViewLayout.collectionViewContentSize.height
}
and then in cellForRowAtIndexpath, just before returning the cell, call this
cell.layoutIfNeeded()
The only solution worked for me when CollectionView is inside TableView custom cell is to
Subclass from ContentSizedCollectionView:
final class ContentSizedCollectionView: UICollectionView {
override var contentSize: CGSize{
didSet {
if oldValue.height != self.contentSize.height {
invalidateIntrinsicContentSize()
}
}
}
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric,
height: contentSize.height)
}
}
private let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.sectionInset = UIEdgeInsets(top: 20, left: 17, bottom: 20, right: 17)
let collectionView = ContentSizedCollectionView(frame: .zero, collectionViewLayout: layout).prepareForAutoLayout()
return collectionView
}()
In UITableViewDelegate for TableView cell:
if let reusableCell = cell as? YourTableViewCell {
reusableCell.frame = tableView.bounds
reusableCell.layoutIfNeeded()
}
Remove height constraints of UICollectionView if any.
This article helped me a lot: https://medium.com/#ar.sarris/self-sizing-collectionview-inside-a-tableview-f1fd4f42c44d
Get the height of the cell. Something like this
let cellHeight = cell.frame.height
Get the origin of the collection view
let cvOrigin = collectionView.frame.origin
Get the width of the collection view
let cvWidth = collectionView.bounds.width
Set the frame of the content view
collection.frame = CGRect(x: cvOrigin.x, y: cvOrigin.y, width: cvWidth, height: cellHeight )

How to make all items in a section of a UICollectionView float over the scroll view?

I want the items of one section in a UICollectionView to remain stationary while the rest of the items inside the UICollectionView are being scrolled.
I tried to achieve this by setting Autolayout constraint that pin the items to the superview of the UICollectionView. However, this does not seem to work because the constraints complain about UICollectionViewCell and the UICollectionView's superview not having a common ancestor.
Is there any other way to achieve it?
Thanks to Ad-J's comment I was able to implement the solution.
I needed to override UICollectionViewFlowLayout and implement the following methods:
override func prepareLayout() {
super.prepareLayout()
//fill layoutInfo of type [NSIndexPath:UICollectionViewLayoutAttributes]
//with layoutAttributes you need in your layout
if let cv = self.collectionView {
for (indexPath, tileToFloat) in layoutInfo {
if indexPath.section == 0 {
var origin = tileToFloat.frame.origin
origin.y += cv.contentOffset.y + cv.contentInset.top
tileToFloat.frame = CGRect(origin: origin, size: tileToFloat.size)
}
tileToFloat.zIndex = 1
}
}
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
This will make all items in the first section stationary.

Resizing UITableView to fit content

I am creating an app which will have a question in a UILabel and a multiple choice answers displayed in UITableView, each row showing a multiple choice. Questions and answers will vary, so I need this UITableView to be dynamic in height.
I would like to find a sizeToFit work around for the table. Where the table's frame is set to the height of all it's content.
Can anyone advise on how I can achieve this?
Swift 5 and 4.2 solution without KVO, DispatchQueue, or setting constraints yourself.
This solution is based on Gulz's answer.
1) Create a subclass of UITableView:
import UIKit
final class ContentSizedTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
2) Add a UITableView to your layout and set constraints on all sides. Set the class of it to ContentSizedTableView.
3) You should see some errors, because Storyboard doesn't take our subclass' intrinsicContentSize into account. Fix this by opening the size inspector and overriding the intrinsicContentSize to a placeholder value. This is an override for design time. At runtime it will use the override in our ContentSizedTableView class
Update: Changed code for Swift 4.2. If you're using a prior version, use UIViewNoIntrinsicMetric instead of UIView.noIntrinsicMetric
Actually I found the answer myself.
I just create a new CGRect for the tableView.frame with the height of table.contentSize.height
That sets the height of the UITableView to the height of its content.
Since the code modifies the UI, do not forget to run it in the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
//This code will run in the main thread:
CGRect frame = self.tableView.frame;
frame.size.height = self.tableView.contentSize.height;
self.tableView.frame = frame;
});
Swift Solution
Follow these steps:
Set the height constraint for the table from the storyboard.
Drag the height constraint from the storyboard and create #IBOutlet for it in the view controller file.
#IBOutlet var tableHeight: NSLayoutConstraint!
Then you can change the height for the table dynamicaly using this code:
override func viewWillLayoutSubviews() {
super.updateViewConstraints()
self.tableHeight?.constant = self.table.contentSize.height
}
If the last row is cut off, try to call viewWillLayoutSubviews() in willDisplay cell function:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
self.viewWillLayoutSubviews()
}
I've tried this in iOS 7 and it worked for me
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView sizeToFit];
}
Add an observer for the contentSize property on the table view, and adjust the frame size accordingly
[your_tableview addObserver:self forKeyPath:#"contentSize" options:0 context:NULL];
then in the callback:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
CGRect frame = your_tableview.frame;
frame.size = your_tableview.contentSize;
your_tableview.frame = frame;
}
Hope this will help you.
I had a table view inside scroll view and had to calculate tableView's height and resize it accordingly. Those are steps I've taken:
0) add a UIView to your scrollView (probably will work without this step but i did it to avoid any possible conflicts) - this will be a containr view for your table view. If you take this step , then set the views borders right to tableview's ones.
1) create a subclass of UITableView:
class IntrinsicTableView: UITableView {
override var contentSize:CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return CGSize(width: UIViewNoIntrinsicMetric, height: contentSize.height)
}
}
2) set class of a table view in Storyboard to IntrinsicTableView: screenshot: http://joxi.ru/a2XEENpsyBWq0A
3) Set the heightConstraint to your table view
4) drag the IBoutlet of your table to your ViewController
5) drag the IBoutlet of your table's height constraint to your ViewController
6) add this method into your ViewController:
override func viewWillLayoutSubviews() {
super.updateViewConstraints()
self.yourTableViewsHeightConstraint?.constant = self.yourTableView.intrinsicContentSize.height
}
Hope this helps
Swift 5 Solution
Follow these four steps:
Set the height constraint for the tableview from the storyboard.
Drag the height constraint from the storyboard and create #IBOutlet for it in the view controller file.
#IBOutlet var tableViewHeightConstraint: NSLayoutConstraint!
Add an observer for the contentSize property on the override func viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
}
Then you can change the height for the table dynamicaly using this code:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if(keyPath == "contentSize"){
if let newvalue = change?[.newKey]
{
DispatchQueue.main.async {
let newsize = newvalue as! CGSize
self.tableViewHeightConstraint.constant = newsize.height
}
}
}
}
In case you don't want to track table view's content size changes yourself, you might find this subclass useful.
protocol ContentFittingTableViewDelegate: UITableViewDelegate {
func tableViewDidUpdateContentSize(_ tableView: UITableView)
}
class ContentFittingTableView: UITableView {
override var contentSize: CGSize {
didSet {
if !constraints.isEmpty {
invalidateIntrinsicContentSize()
} else {
sizeToFit()
}
if contentSize != oldValue {
if let delegate = delegate as? ContentFittingTableViewDelegate {
delegate.tableViewDidUpdateContentSize(self)
}
}
}
}
override var intrinsicContentSize: CGSize {
return contentSize
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
return contentSize
}
}
In case your contentSize is not correct this is because it is based on the estimatedRowHeight (automatic), use this before
tableView.estimatedRowHeight = 0;
source : https://forums.developer.apple.com/thread/81895
I did in a bit different way, Actually my TableView was inside scrollview so i had to give height constraint as 0.
Then at runtime I made following changes,
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
self.viewWillLayoutSubviews()
}
override func viewWillLayoutSubviews() {
super.updateViewConstraints()
DispatchQueue.main.async {
self.tableViewHeightConstraint?.constant = self.myTableView.contentSize.height
self.view.layoutIfNeeded()
}
}
Swift 3, iOS 10.3
Solution 1:
Just put self.tableview.sizeToFit() in cellForRowAt indexPath function. Make sure to set tableview height higher then you need.
This is a good solution if you don't have views below tableview. However, if you have, bottom tableview constraint will not be updated (I didn't try to fix it because I came up with solution 2)
Example:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as? TestCell {
cell.configureCell(data: testArray[indexPath.row])
self.postsTableView.sizeToFit()
return cell
}
return UITableViewCell()
}
Solution 2:
Set tableview height constraint in storyboard and drag it to the ViewController. If you know the average height of your cell and you know how many elements your array contains, you can do something like this:
tableViewHeightConstraint.constant = CGFloat(testArray.count) * 90.0 // Let's say 90 is the average cell height
*EDIT:
After all the solutions I tried and every of them was fixing something, but not completely, this is the answer that explains and fixes this problem completely.
This works for me using Auto Layout, with a table view with only one section.
func getTableViewContentHeight(tableView: UITableView) -> CGFloat {
tableView.bounds = CGRect(x: 0, y: 0, width: 300, height: 40)
let rows = tableView.numberOfRows(inSection: 0)
var height = CGFloat(0)
for n in 0...rows - 1 {
height = height + tableView.rectForRow(at: IndexPath(row: n, section: 0)).height
}
return height
}
I call this function when setting up Auto Layout (The sample here uses SnapKit, but you get the idea):
let height = getTableViewContentHeight(tableView: myTableView)
myTableView.snp.makeConstraints {
...
...
$0.height.equalTo(height)
}
I want the UITableView only to be as tall as the combined height of the cells; I loop through the cells and accumulate the total height of the cells. Since the size of the table view is CGRect.zero at this point, I need to set the bounds to be able to respect the Auto Layout rules defined by the cell. I set the size to an arbitrary value that should be large enough. The actual size will be calculated later by the Auto Layout system.
There is a much better way to do it if you use AutoLayout: change the constraint that determines the height. Just calculate the height of your table contents, then find the constraint and change it. Here's an example (assuming that the constraint that determines your table's height is actually a height constraint with relation "Equal"):
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
for constraint in tableView.constraints {
if constraint.firstItem as? UITableView == tableView {
if constraint.firstAttribute == .height {
constraint.constant = tableView.contentSize.height
}
}
}
}
based on
fl034's answer
SWift 5
var tableViewHeight: NSLayoutConstraint?
tableViewHeight = NSLayoutConstraint(item: servicesTableView,
attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute,
multiplier: 0.0, constant: 10)
tableViewHeight?.isActive = true
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
tableViewHeight?.constant = tableView.contentSize.height
tableView.layoutIfNeeded()
}
Mimo's answer and Anooj VM 's answer both are awesome but there is a small problem if you have a large list, it's possible that the height of the frame will cutoff some of your cells.
So. I have modified the answer a little bit:
dispatch_async(dispatch_get_main_queue()) {
//This code will run in the main thread:
CGFloat newHeight=self.tableView.contentSize.height;
CGFloat screenHeightPermissible=(self.view.bounds.size.height-self.tableView.frame.origin.y);
if (newHeight>screenHeightPermissible)
{
//so that table view remains scrollable when 'newHeight' exceeds the screen bounds
newHeight=screenHeightPermissible;
}
CGRect frame = self.tableView.frame;
frame.size.height = newHeight;
self.tableView.frame = frame;
}
My Swift 5 implementation is to set the hight constraint of the tableView to the size of its content (contentSize.height). This method assumes you are using auto layout. This code should be placed inside the cellForRowAt tableView method.
tableView.heightAnchor.constraint(equalToConstant: tableView.contentSize.height).isActive = true
For my case, how I manage is.
give any constant height of table view. create outlet of table view height and then call the following function where ever you relaod the tableView.
private func manageHeight(){
tableViewHeight.constant=CGFloat.greatestFiniteMagnitude
tableView.reloadData()
tableView.layoutIfNeeded()
tableViewHeight.constant=tableView.contentSize.height
}
note: tableView is the outlet for your table view and tableViewHeight is the outlet for tableView height.
As an extension of Anooj VM's answer, I suggest the following to refresh content size only when it changes.
This approach also disable scrolling properly and support larger lists and rotation. There is no need to dispatch_async because contentSize changes are dispatched on main thread.
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView addObserver:self forKeyPath:#"contentSize" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:NULL];
}
- (void)resizeTableAccordingToContentSize:(CGSize)newContentSize {
CGRect superviewTableFrame = self.tableView.superview.bounds;
CGRect tableFrame = self.tableView.frame;
BOOL shouldScroll = newContentSize.height > superviewTableFrame.size.height;
tableFrame.size = shouldScroll ? superviewTableFrame.size : newContentSize;
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
self.tableView.frame = tableFrame;
} completion: nil];
self.tableView.scrollEnabled = shouldScroll;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([change[NSKeyValueChangeKindKey] unsignedIntValue] == NSKeyValueChangeSetting &&
[keyPath isEqualToString:#"contentSize"] &&
!CGSizeEqualToSize([change[NSKeyValueChangeOldKey] CGSizeValue], [change[NSKeyValueChangeNewKey] CGSizeValue])) {
[self resizeTableAccordingToContentSize:[change[NSKeyValueChangeNewKey] CGSizeValue]];
}
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
[self resizeTableAccordingToContentSize:self.tableView.contentSize]; }
- (void)dealloc {
[self.tableView removeObserver:self forKeyPath:#"contentSize"];
}
objc version of Musa almatri
(void)viewWillLayoutSubviews
{
[super updateViewConstraints];
CGFloat desiredHeight = self.tableView.contentSize.height;
// clamp desired height, if needed, and, in that case, leave scroll Enabled
self.tableHeight.constant = desiredHeight;
self.tableView.scrollEnabled = NO;
}
You can try Out this Custom AGTableView
To Set a TableView Height Constraint Using storyboard or programmatically. (This class automatically fetch a height constraint and set content view height to yourtableview height).
class AGTableView: UITableView {
fileprivate var heightConstraint: NSLayoutConstraint!
override init(frame: CGRect, style: UITableViewStyle) {
super.init(frame: frame, style: style)
self.associateConstraints()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.associateConstraints()
}
override open func layoutSubviews() {
super.layoutSubviews()
if self.heightConstraint != nil {
self.heightConstraint.constant = self.contentSize.height
}
else{
self.sizeToFit()
print("Set a heightConstraint to Resizing UITableView to fit content")
}
}
func associateConstraints() {
// iterate through height constraints and identify
for constraint: NSLayoutConstraint in constraints {
if constraint.firstAttribute == .height {
if constraint.relation == .equal {
heightConstraint = constraint
}
}
}
}
}
Note If any problem to set a Height then yourTableView.layoutSubviews().
Based on answer of fl034. But for Xamarin.iOS users:
[Register("ContentSizedTableView")]
public class ContentSizedTableView : UITableView
{
public ContentSizedTableView(IntPtr handle) : base(handle)
{
}
public override CGSize ContentSize { get => base.ContentSize; set { base.ContentSize = value; InvalidateIntrinsicContentSize(); } }
public override CGSize IntrinsicContentSize
{
get
{
this.LayoutIfNeeded();
return new CGSize(width: NoIntrinsicMetric, height: ContentSize.Height);
}
}
}
I am using a UIView extension , approach is close to #ChrisB approach above
extension UIView {
func updateHeight(_ height:NSLayoutConstraint)
{
let newSize = CGSize(width: self.frame.size.width, height: CGFloat(MAXFLOAT))
let fitSize : CGSize = self.sizeThatFits(newSize)
height.constant = fitSize.height
}
}
implementation : :
#IBOutlet weak var myTableView: UITableView!
#IBOutlet weak var myTableVieweHeight: NSLayoutConstraint!
//(call it whenever tableView is updated inside/outside delegate methods)
myTableView.updateHeight(myTableVieweHeigh)
Bonus : Can be used on any other UIViews eg:your own dynamic label
If you want your table to be dynamic, you will need to use a solution based on the table contents as detailed above. If you simply want to display a smaller table, you can use a container view and embed a UITableViewController in it - the UITableView will be resized according to the container size.
This avoids a lot of calculations and calls to layout.
Mu solution for this in swift 3: Call this method in viewDidAppear
func UITableView_Auto_Height(_ t : UITableView)
{
var frame: CGRect = t.frame;
frame.size.height = t.contentSize.height;
t.frame = frame;
}

Resources