Dynamically size Table View Cells using Auto Layout constraints - ios

Update
I have revised the question completely after my latest findings.
Goal
My goal is to implement the following effect:
There is a simple table view
The user selects a row
The selected row expands, revealing another label below the original one
Please note that I am aware, that this can be achieved by inserting/deleting cells below the selected one, I already have a successful implementation using that method.
This time, I want to try to achieve this using Auto Layout constraints.
Current status
I have a sample project available for anyone to check, and also opened an issue. To summarize, here's what I've tried so far:
I have the following views as actors here:
The cell's content view
A top view, containing the main label ("main view")
A bottom view, below the main view, containing the initially hidden label ("detail view")
I have set up Auto Layout constraints within my cell the following way (please note that this is strictly pseudo-language):
mainView.top = contentView.top
mainView.leading = contentView.leading
mainView.trailing = contentView.trailing
mainView.bottom = detailView.top
detailView.leading = contentView.leading
detailView.trailing = contentView.trailing
detailView.bottom = contentView.bottom
detailView.height = 0
I have a custom UITableViewCell subclass, with multiple outlets, but the most important here is an outlet for the height constraint mentioned previously: the idea here is to set its constant to 0 by default, but when the cell is selected, set it to 44, so it becomes visible:
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
detailViewHeightConstraint.constant = selected ? detailViewDefaultHeight : 0
UIView.animateWithDuration(0.3) {
self.layoutIfNeeded()
}
}
I have the following result:
So the effect is working, but not exactly how I originally imagined. Instead of pushing the main view up, I want the cell's height to grow when the detail view is shown, and shrink back when it's hidden.
I have examined my layout hierarchy during runtime:
The initial state is OK. The height of the content view is equal to the height of my main view (in this case, it's 125 points).
When the cell is selected, the height constraint of the detail view is increased to 44 points and the two views are properly stacked vertically.But instead of the cell's content view extending, but instead, the main view shrinks.
Question
What I need is the following: the height of table view cell's content view should be equal to
the height of the main view, when the detail view's height constraint is 0 (currently this works)
main view height + detail view height when the detail view's height constraint is set properly (this does not work).
How do I have to set my constraints to achieve that?

After a significant amount of research, I think I've found the solution with the help of this great article.
Here are the steps needed to make the cell resize:
Within the Main, and Detail Views, I have originally set the labels to be horizontally and vertically centered. This isn't enough for self sizing cells. The first thing I needed is to set up my layout using vertical spacing constraints instead of simple alignment:
Additionally you should set the Main Container's vertical compression resistance to 1000.
The detail view is a bit more tricky: Apart from creating the appropriate vertical constraints, you also have to play with their priorities to reach the desired effect:
The Detail Container's Height is constrained to be 44 points, but to make it optional, set its priority to 999 (according to the docs, anything lower than "Required", will be regarded such).
Within the Detail Container, set up the vertical spacing constraints, and give them a priority of 998.
The main idea is the following:
By default, the cell is collapsed. To achieve this, we must programmatically set the constant of the Detail Container's height constraint to 0. Since its priority is higher than the vertical constraints within the cell's content view, the latter will be ignored, so the Detail Container will be hidden.
When we select the cell, we want it to expand. This means, that the vertical constraints must take control: we set the priority Detail Container's height constraint to something low (I used 250), so it will be ignored in favor of the constraints within the content view.
I had to modify my UITableViewCell subclass to support these operations:
// `showDetails` is exposed to control, whether the cell should be expanded
var showsDetails = false {
didSet {
detailViewHeightConstraint.priority = showsDetails ? lowLayoutPriority : highLayoutPriority
}
}
override func awakeFromNib() {
super.awakeFromNib()
detailViewHeightConstraint.constant = 0
}
To trigger the behavior, we must override tableView(_:didSelectRowAtIndexPath:):
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: false)
switch expandedIndexPath {
case .Some(_) where expandedIndexPath == indexPath:
expandedIndexPath = nil
case .Some(let expandedIndex) where expandedIndex != indexPath:
expandedIndexPath = nil
self.tableView(tableView, didSelectRowAtIndexPath: indexPath)
default:
expandedIndexPath = indexPath
}
}
Notice that I've introduced expandedIndexPath to keep track of our currently expanded index:
var expandedIndexPath: NSIndexPath? {
didSet {
switch expandedIndexPath {
case .Some(let index):
tableView.reloadRowsAtIndexPaths([index], withRowAnimation: UITableViewRowAnimation.Automatic)
case .None:
tableView.reloadRowsAtIndexPaths([oldValue!], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
}
Setting the property will result in the table view reloading the appropriate indexes, giving us a perfect opportunity to tell the cell, if it should expand:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! ExpandableTableViewCell
cell.mainTitle = viewModel.mainTitleForRow(indexPath.row)
cell.detailTitle = viewModel.detailTitleForRow(indexPath.row)
switch expandedIndexPath {
case .Some(let expandedIndexPath) where expandedIndexPath == indexPath:
cell.showsDetails = true
default:
cell.showsDetails = false
}
return cell
}
The last step is to enable self-sizing in viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
tableView.contentInset.top = statusbarHeight
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 125
}
Here is the result:
Cells now correctly size themselves. You may notice that the animation is still a bit weird, but fixing that does not fall into the scope of this question.
Conclusion: this was way harder than it should be. 😀 I really hope to see some improvements in the future.

This is in obj-c, but I'm sure you'll handle that:
Add in your viewDidLoad:
self.tableView.estimatedRowHeight = self.tableView.rowHeight;
self.tableView.rowHeight = UITableViewAutomaticDimension;
This will enable self sizing cells for your tableView, and should work on iOS8+

Related

How to manually load UITableView

I kind of have a weird layout here, it's kind of like this (also see pics):
-UITableViewCell 1
----UIView 2
--------UITableView 3
The controller the the UITableView (1) is like that:
//mainTableView (1) controller
var cellHeights = [CGFloat]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let card = CardSource.orangeCards[indexPath.row]
cell.configureCardInCell(card)
cellHeights.insert(card.frame.height + 15, at: indexPath.row)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row]
}
but the problem is that when the screen first loads, the UIViews overlap because the cells are too small because the smaller table view (the one in the UIView) hasn't loaded yet and it's height isn't defined. Proof of this is that when I scroll to the bottom of the main table view then scroll back up cellForRowAt is called again and the two views don't overlap anymore (see pics). So what I basically want is a way to load the small table view and define it's height before the bigger table view loads (or if you have any other solutions, that'd be welcome too)
I know my question isn't very clear, I'm not really good at explaining stuff, so don't hesitate to ask me questions in the comments.
Many thanks!
When the view first loads
After scrolling down then back up
EDIT:
I found this:
static var pharmacyOrangeCard: CardView {
let view = Bundle.main.loadNibNamed("Pharmacy Orange Card", owner: self, options: nil)?.first as! PharmacyTableCardView
print(view.frame.height)
return view
}
prints the correct height. But then, when I try to access it from the controller above, it gives me a smaller number! In the meanwhile, I applied these constraints:
self.translatesAutoresizingMaskIntoConstraints = false
self.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 35).isActive = true
card.centerXAnchor.constraint(equalTo: cell.centerXAnchor).isActive = true
card.topAnchor.constraint(equalTo: cell.topAnchor).isActive = true
But I don't think that affects height, does it?
EDIT 2:
Okay, so I've changed this constraint:
self.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 35).isActive = true
to this:
card.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 17.5).isActive = true
card.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -17.5).isActive = true
So these constraints seem to play a role because now I have this:
enter image description here
By the way, I don't know if that matters but I'm using XIB files for each of these "cards", and the height isn't constrained, so maybe that plays a role?
SOLVING EDIT:
I solved the problem by doing:
override func viewDidAppear(_ animated: Bool) {
mainTableView.reloadData()
}
Once a cell loaded on the screen, you cannot change height for that cell for better UI-Experience,
and in hierarchy heightForRowAt get called before cellForRowAt.
So you had 2 options to choose for a solution to your problem
first:: get your heights values ready before your table view try to loads cells in it (get heights array ready before setting delegate and datasource values to your tableView)
second:: whenever you need to update your tableView cells to re-established with respect to new height values, call this each time after you have updated your height values
yourTableView.reloadData()

Autolayout: Can't figure out how to resolve warnings relating to expanding a panel inside a UITableViewCell

I have the following layout for UITableViewCell:
The layout consists of two subviews:
TopView (containing a Show button)
BottomView (which is expanded or collapsed when the Show button is pressed).
BottomView consists of 3 subviews. The constraints of these subviews are:
Each of the three subviews contains a UILabel that is pinned to the leading,top and trailing edges with a constant == 8.
The UILabel is pinned to the bottom of the UIView using a constraint that is >= 8. This forces the UILabel to be aligned to the top of the UIView.
The left-most of the three UIViews is pinned to the leading edge of BottomView.
The right-most of the three UIViews is pinned to the trailing edge of BottomView
Each of the three UIViews is pinned to the top of BottomView
Each of the three UIViews is pinned to the bottom of BottomView
The three views have equal widths and equal heights.
the bottom of BottomView is pinned to the the bottom of the UITableViewCell's Content View
This gives me my desired layout:
What I'd like to accomplish is the following:
Initially, BottomView should be collapsed.
Clicking on the Show button should expand or collapse the BottomView as appropriate.
I managed to do this by creating a bottomViewHeightConstraint that is initially uninstalled. Tapping on the show button activates/deactivates the constraint.
AnotherTableViewCell.m
-(IBAction) show
{
self.bottomViewHeightConstraint.active = !self.bottomViewHeightConstraint.active;
[UIView animateWithDuration:0.3 animations:^{
[self layoutIfNeeded];
[self.delegate cellRequiresUpdates:self];
} completion:^(BOOL finished) {
}];
}
UIViewController.m
-(void) cellRequiresUpdates:(AnotherTableViewCell *)cell
{
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
This worked but produced a lot of warnings for unsatisfiable constraints. I'd like help in understanding which of my constraints are causing the warnings.
The unsatisfiable constraints are:
bottomViewHeightConstraint== 0
(needed because I want to collapse bottom view)
Equal heights between the left-most and right most UIView in UIBottomView
(tried deactivating, but warning didn't go away)
8-pixel distance between the bottom of the leftmost UIView and the bottom of BottomView
(tried deactivating, but warning didn't go away)
8-pixel distance between the bottom of the leftmost UIView and the bottom of BottomView
(tried deactivating, but warning didn't go away)
OK - this could solve your issue. Working on a similar recent question, I came across this in Apple's docs:
NOTE Don’t feel obligated to use all 1000 priority values. In fact, priorities should general cluster around the system-defined low (250), medium (500), high (750), and required (1000) priorities. You may need to make constraints that are one or two points higher or lower than these values, to help prevent ties. If you’re going much beyond that, you probably want to reexamine your layout’s logic.
Of course, when creating constraints in IB, they appear to use 1000 by default.
So, for your particular layout...
Select all the constraints on the Bottom View, except the Bottom View Height Constraint:
and change the Priority to 998
Then, select only the Bottom View Height Constraint and set its Priority to 999
Run your app and show/hide the bottom view to your heart's content :)
Ref: https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/AnatomyofaConstraint.html#//apple_ref/doc/uid/TP40010853-CH9-SW19
Some rough & ready code does what you're after -
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var expanded: [Bool] = [false, false, false, false]
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 64.0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! ExpandingCell
cell.isExpanded = self.expanded[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? ExpandingCell {
let isExpanded = !self.expanded[indexPath.row]
self.expanded[indexPath.row] = isExpanded
cell.isExpanded = isExpanded
tableView.beginUpdates()
tableView.endUpdates()
}
}
}
class ExpandingCell: UITableViewCell {
#IBOutlet weak var label: UILabel!
public var isExpanded = false {
didSet {
if self.isExpanded {
self.label.text = "Lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots & lots of text"
} else {
self.label.text = "Not a lot of text"
}
}
}
}
Just remember that your view in the cell has to be constrained to the top & bottom of the contentView, autolayout done right means that when you change the content of the label in this case, the cell will resize correctly.

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.

UITableViewCell height is not fitted when hiding UIView inside the cell using AutoLayout

I have been struggling this issue for 3 days and still can not figure it out. I do hope anyone here can help me.
Currently, i have an UITableView with customized cell(subclass of UITableViewCell) on it. Within this customized cell, there are many UILabels and all of them are set with Auto Layout (pining to cell content view) properly. By doing so, the cell height could display proper height no matter the UILabel is with long or short text.
The problem is that when i try to set one of the UILabels (the bottom one) to be hidden, the content view is not adjusted height accordingly and so as cell.
What i have down is i add an Hight Constraint from Interface Builder to that bottom label with following.
Priority = 250 (Low)
Constant = 0
Multiplier = 1
This make the constrain with the dotted line. Then, in the Swift file, i put following codes.
override func viewDidLoad() {
super.viewDidLoad()
//Setup TableView
tableView.allowsMultipleSelectionDuringEditing = true
//For tableView cell resize with autolayout
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 200
}
func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
let cell = tableView.cellForRowAtIndexPath(indexPath) as! RecordTableViewCell
cell.lbLine.hidden = !cell.lbLine.hidden
if cell.lbLine.hidden != true{
//show
cell.ConstrainHeightForLine.priority = 250
}else{
//not show
cell.ConstrainHeightForLine.priority = 999
}
//tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
return indexPath
}
The tricky thing is that when i call tableView.reloadRowAtIndexPaths(), the cell would display the correct height but with a bug that it has to be trigger by double click (selecting) on the same row rather than one click.
For this, i also try following code inside the willSelectRowAtIndexPath method, but none of them is worked.
cell.contentView.setNeedsDisplay()
cell.contentView.layoutIfNeeded()
cell.contentView.setNeedsUpdateConstraints()
Currently the result is as following (with wrong cell Height):
As showed in the Figure 2, UILabel 6 could be with long text and when i hide this view, the content view is still showing as large as it before hiding.
Please do point me out where i am wrong and i will be appreciated.
I finally change the code
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
to the following
tableView.reloadData()
Then, it work perfectly.
However, i don't really know the exactly reason on it. Hope someone can still comment it out.

UICollectionView inside a UITableViewCell -- dynamic height?

One of our application screens requires us to place a UICollectionView inside of a UITableViewCell. This UICollectionView will have a dynamic number of items, resulting in a height which must be calculated dynamically as well. However, I am running into problems trying to calculate the height of the embedded UICollectionView.
Our overarching UIViewController was created in Storyboards and does make use of auto layout. But, I don't know how to dynamically increase the height of the UITableViewCell based on the height of the UICollectionView.
Can anyone give some tips or advice on how to accomplish this?
The right answer is YES, you CAN do this.
I came across this problem some weeks ago. It is actually easier than you may think. Put your cells into NIBs (or storyboards) and pin them to let auto layout do all the work
Given the following structure:
TableView
TableViewCell
CollectionView
CollectionViewCell
CollectionViewCell
CollectionViewCell
[...variable number of cells or different cell sizes]
The solution is to tell auto layout to compute first the collectionViewCell sizes, then the collection view contentSize, and use it as the size of your cell. This is the UIView method that "does the magic":
-(void)systemLayoutSizeFittingSize:(CGSize)targetSize
withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority
verticalFittingPriority:(UILayoutPriority)verticalFittingPriority
You have to set here the size of the TableViewCell, which in your case is the CollectionView's contentSize.
CollectionViewCell
At the CollectionViewCell you have to tell the cell to layout each time you change the model (e.g.: you set a UILabel with a text, then the cell has to be layout again).
- (void)bindWithModel:(id)model {
// Do whatever you may need to bind with your data and
// tell the collection view cell's contentView to resize
[self.contentView setNeedsLayout];
}
// Other stuff here...
TableViewCell
The TableViewCell does the magic. It has an outlet to your collectionView, enables the auto layout for collectionView cells using estimatedItemSize of the UICollectionViewFlowLayout.
Then, the trick is to set your tableView cell's size at the systemLayoutSizeFittingSize... method. (NOTE: iOS8 or later)
NOTE: I tried to use the delegate cell's height method of the tableView -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.but it's too late for the auto layout system to compute the CollectionView contentSize and sometimes you may find wrong resized cells.
#implementation TableCell
- (void)awakeFromNib {
[super awakeFromNib];
UICollectionViewFlowLayout *flow = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
// Configure the collectionView
flow.minimumInteritemSpacing = ...;
// This enables the magic of auto layout.
// Setting estimatedItemSize different to CGSizeZero
// on flow Layout enables auto layout for collectionView cells.
// https://developer.apple.com/videos/play/wwdc2014-226/
flow.estimatedItemSize = CGSizeMake(1, 1);
// Disable the scroll on your collection view
// to avoid running into multiple scroll issues.
[self.collectionView setScrollEnabled:NO];
}
- (void)bindWithModel:(id)model {
// Do your stuff here to configure the tableViewCell
// Tell the cell to redraw its contentView
[self.contentView layoutIfNeeded];
}
// THIS IS THE MOST IMPORTANT METHOD
//
// This method tells the auto layout
// You cannot calculate the collectionView content size in any other place,
// because you run into race condition issues.
// NOTE: Works for iOS 8 or later
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority {
// With autolayout enabled on collection view's cells we need to force a collection view relayout with the shown size (width)
self.collectionView.frame = CGRectMake(0, 0, targetSize.width, MAXFLOAT);
[self.collectionView layoutIfNeeded];
// If the cell's size has to be exactly the content
// Size of the collection View, just return the
// collectionViewLayout's collectionViewContentSize.
return [self.collectionView.collectionViewLayout collectionViewContentSize];
}
// Other stuff here...
#end
TableViewController
Remember to enable the auto layout system for the tableView cells at your TableViewController:
- (void)viewDidLoad {
[super viewDidLoad];
// Enable automatic row auto layout calculations
self.tableView.rowHeight = UITableViewAutomaticDimension;
// Set the estimatedRowHeight to a non-0 value to enable auto layout.
self.tableView.estimatedRowHeight = 10;
}
CREDIT: #rbarbera helped to sort this out
I think my solution is much simpler than the one proposed by #PabloRomeu.
Step 1. Create outlet from UICollectionView to UITableViewCell subclass, where UICollectionView is placed. Let, it's name will be collectionView
Step 2. Add in IB for UICollectionView height constraint and create outlet to UITableViewCell subclass too. Let, it's name will be collectionViewHeight.
Step 3. In tableView:cellForRowAtIndexPath: add code:
// deque a cell
cell.frame = tableView.bounds;
[cell layoutIfNeeded];
[cell.collectionView reloadData];
cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height;
Both table views and collection views are UIScrollView subclasses and thus don't like to be embedded inside another scroll view as they try to calculate content sizes, reuse cells, etc.
I recommend you to use only a collection view for all your purposes.
You can divide it in sections and "treat" some sections' layout as a table view and others as a collection view. After all there's nothing you can't achieve with a collection view that you can with a table view.
If you have a basic grid layout for your collection view "parts" you can also use regular table cells to handle them. Still if you don't need iOS 5 support you should better use collection views.
I read through all the answers. This seems to serve all cases.
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
collectionView.layoutIfNeeded()
collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width , height: 1)
return collectionView.collectionViewLayout.collectionViewContentSize
}
Pablo Romeu's answer above (https://stackoverflow.com/a/33364092/2704206) helped me immensely with my issue. I had to do a few things differently, however, to get this working for my problem. First off, I didn't have to call layoutIfNeeded() as often. I only had to call it on the collectionView in the systemLayoutSizeFitting function.
Secondly, I had auto layout constraints on my collection view in the table view cell to give it some padding. So I had to subtract the leading and trailing margins from the targetSize.width when setting the collectionView.frame's width. I also had to add the top and bottom margins to the return value CGSize height.
To get these constraint constants, I had the option of either creating outlets to the constraints, hard-coding their constants, or looking them up by an identifier. I decided to go with the third option to make my custom table view cell class easily reusable. In the end, this was everything I needed to get it working:
class CollectionTableViewCell: UITableViewCell {
// MARK: -
// MARK: Properties
#IBOutlet weak var collectionView: UICollectionView! {
didSet {
collectionViewLayout?.estimatedItemSize = CGSize(width: 1, height: 1)
selectionStyle = .none
}
}
var collectionViewLayout: UICollectionViewFlowLayout? {
return collectionView.collectionViewLayout as? UICollectionViewFlowLayout
}
// MARK: -
// MARK: UIView functions
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
collectionView.layoutIfNeeded()
let topConstraintConstant = contentView.constraint(byIdentifier: "topAnchor")?.constant ?? 0
let bottomConstraintConstant = contentView.constraint(byIdentifier: "bottomAnchor")?.constant ?? 0
let trailingConstraintConstant = contentView.constraint(byIdentifier: "trailingAnchor")?.constant ?? 0
let leadingConstraintConstant = contentView.constraint(byIdentifier: "leadingAnchor")?.constant ?? 0
collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width - trailingConstraintConstant - leadingConstraintConstant, height: 1)
let size = collectionView.collectionViewLayout.collectionViewContentSize
let newSize = CGSize(width: size.width, height: size.height + topConstraintConstant + bottomConstraintConstant)
return newSize
}
}
As a helper function to retrieve a constraint by identifier, I add the following extension:
extension UIView {
func constraint(byIdentifier identifier: String) -> NSLayoutConstraint? {
return constraints.first(where: { $0.identifier == identifier })
}
}
NOTE: You will need to set the identifier on these constraints in your storyboard, or wherever they are being created. Unless they have a 0 constant, then it doesn't matter. Also, as in Pablo's response, you will need to use UICollectionViewFlowLayout as the layout for your collection view. Finally, make sure you link the collectionView IBOutlet to your storyboard.
With the custom table view cell above, I can now subclass it in any other table view cell that needs a collection view and have it implement the UICollectionViewDelegateFlowLayout and UICollectionViewDataSource protocols. Hope this is helpful to someone else!
An alternative to Pablo Romeu's solution is to customise UICollectionView itself, rather than doing the work in table view cell.
The underlying problem is that by default a collection view has no intrinsic size and so cannot inform auto layout of the dimensions to use. You can remedy that by creating a custom subclass which does return a useful intrinsic size.
Create a subclass of UICollectionView and override the following methods
override func intrinsicContentSize() -> CGSize {
self.layoutIfNeeded()
var size = super.contentSize
if size.width == 0 || size.height == 0 {
// return a default size
size = CGSize(width: 600, height:44)
}
return size
}
override func reloadData() {
super.reloadData()
self.layoutIfNeeded()
self.invalidateIntrinsicContentSize()
}
(You should also override the related methods: reloadSections, reloadItemsAtIndexPaths in a similar way to reloadData())
Calling layoutIfNeeded forces the collection view to recalculate the content size which can then be used as the new intrinsic size.
Also, you need to explicitly handle changes to the view size (e.g. on device rotation) in the table view controller
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
Easiest approach I've came up with, so far, Credits to #igor answer above,
In your tableviewcell class just insert this
override func layoutSubviews() {
self.collectionViewOutlet.constant = self.postPoll.collectionViewLayout.collectionViewContentSize.height
}
and of course, change the collectionviewoutlet with your outlet in the cell's class
I was facing the same issue recently and I almost tried every solution in the answers, some of them worked and others didn't my main concern about #PabloRomeu approach is that if you have other contents in the cell (other than the collection view) you will have to calculate their heights and the heights of their constraints and return the result to get the auto layout right and I don't like to calculate things manually in my code. So here is the solution that worked fine for me without doing any manual calculations in my code.
in the cellForRow:atIndexPath of the table view I do the following:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//do dequeue stuff
//initialize the the collection view data source with the data
cell.frame = CGRect.zero
cell.layoutIfNeeded()
return cell
}
I think what happens here is that I force the tableview cell to adjust its height after the collection view height has been calculated. (after providing the collectionView date to the data source)
I would put a static method on the collection view class that will return a size based on the content it will have. Then use that method in the heightForRowAtIndexPath to return the proper size.
Also note that you can get some weird behavior when you embed these kinds of viewControllers. I did it once and had some weird memory issues I never worked out.
Maybe my variant will be useful; i've been deciding this task during last two hours. I don't pretend it's 100% correct or optimal, but my skill's very small yet and i'd like to hear comments from experts. Thank you.
One important note: this works for static table - it's specified by my current work.
So, all I use is viewWillLayoutSubviews of tableView. And a little bit more.
private var iconsCellHeight: CGFloat = 500
func updateTable(table: UITableView, withDuration duration: NSTimeInterval) {
UIView.animateWithDuration(duration, animations: { () -> Void in
table.beginUpdates()
table.endUpdates()
})
}
override func viewWillLayoutSubviews() {
if let iconsCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 1)) as? CategoryCardIconsCell {
let collectionViewContentHeight = iconsCell.iconsCollectionView.contentSize.height
if collectionViewContentHeight + 17 != iconsCellHeight {
iconsCellHeight = collectionViewContentHeight + 17
updateTable(tableView, withDuration: 0.2)
}
}
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch (indexPath.section, indexPath.row) {
case ...
case (1,0):
return iconsCellHeight
default:
return tableView.rowHeight
}
}
I know, that the collectionView is located in the first row of the second section;
Let the height of the row is 17 p. bigger, than its content height;
iconsCellHeight is a random number as the program starts (i know, that in the portrait form it has to be exactly 392, but it's not important). If the content of collectionView + 17 is not equal this number, so change its value. Next time in this situation the condition gives FALSE;
After all update the tableView. In my case its the combination of two operations (for nice updating of extending rows);
And of course, in the heightForRowAtIndexPath add one row to code.
I get idea from #Igor post and invest my time to this for my project with swift
Just past this in your
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//do dequeue stuff
cell.frame = tableView.bounds
cell.layoutIfNeeded()
cell.collectionView.reloadData()
cell.collectionView.heightAnchor.constraint(equalToConstant: cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
cell.layoutIfNeeded()
return cell
}
Addition:
If you see your UICollectionView choppy when loading cells.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//do dequeue stuff
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
return cell
}
Pablo's solution did not work very well for me, I had strange visual effects (the collectionView not adjusting correctly).
What worked was to adjust the height constraint of the collectionView (as a NSLayoutConstraint) to the collectionView contentSize during layoutSubviews(). This is the method called when autolayout is applied to the cell.
// Constraint on the collectionView height in the storyboard. Priority set to 999.
#IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
// Method called by autolayout to layout the subviews (including the collectionView).
// This is triggered with 'layoutIfNeeded()', or by the viewController
// (happens between 'viewWillLayoutSubviews()' and 'viewDidLayoutSubviews()'.
override func layoutSubviews() {
collectionViewHeightConstraint.constant = collectionView.contentSize.height
super.layoutSubviews()
}
// Call `layoutIfNeeded()` when you update your UI from the model to trigger 'layoutSubviews()'
private func updateUI() {
layoutIfNeeded()
}
func configure(data: [Strings]) {
names = data
contentView.layoutIfNeeded()
collectionviewNames.reloadData()
}
Short and sweet. Consider the above method in your tableViewCell class. You would probably call it from func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell after dequeing your cell. Before calling reloadData on your collection view, in your tableCell, you need to tell the collection view to lay out its subviews, if layout updates are pending.
In your UITableViewDelegate:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return ceil(itemCount/4.0f)*collectionViewCellHeight;
}
Substitute itemCount and CollectionViewCellHeight with the real values. If you have an array of arrays itemCount might be:
self.items[indexPath.row].count
Or whatever.
1.Create dummy cell.
2.Use collectionViewContentSize method on UICollectionViewLayout of UICollectionView using current data.
You can calculate the height of the collection based on its properties like itemSize, sectionInset, minimumLineSpacing, minimumInteritemSpacing, if your collectionViewCell has the border of a rule.

Resources