My situation:
I have a horizontal ScrollView containing a StackView.
Inside this StackView there are some Views, that can be expanded/collapsed.
When I want to expand one of these Views, I first unhide some subViews in the View. After that I need to change the height of the ScrollView based on the new height of this View.
But this is not working...
I try this code:
UIView.animate(withDuration: 0.3) { [self] in
// Toggle hight of all subViews
stackView.arrangedSubviews.forEach { itemView in
guard let itemView = itemView as? MyView else { return }
itemView.toggleView()
}
// Now update the hight of the StackView
// But here the hight is always from the previous toggle
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
print(height)
heightConstraint.constant = height
}
This code nicely animates, but always to the wrong height.
So the ScrollView animates to collapsed when it should be expanded and expanded when it should be collapsed.
Anyone with on idea how to solve this?
The problem is that, whatever you are doing here:
itemView.toggleView()
may have done something to change the height a view, but then you immediately call:
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
before UIKit has updated the frames.
So, you can either track your own height property, or...
get the frame heights after the update - such as with:
DispatchQueue.main.async {
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
print("h", height)
self.scrollHeightConstraint.constant = height
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
I have a tableview cell containing a custom view among other views and autolayout is used.
The purpose of my custom view is to layout its subviews in rows and breaks into a new row if the current subview does not fit in the current line. It kind of works like a multiline label but with views. I achieved this through exact positioning instead of autolayout.
Since I only know the width of my view in layoutSubviews(), I need to calculate the exact positions and number of lines there. This worked out well, but the frame(zero) of my view didn't match because of missing intrinsicContentSize.
So I added a check to the end of my calculation if my height changed since the last layout pass. If it did I update the height property which is used in my intrinsicContentSize property and call invalidateIntrinsicContentSize().
I observed that initially layoutSubviews() is called twice. The first pass works well and the intrinsicContentSize is taken into account even though the width of the cell is smaller than it should be. The second pass uses the actual width and also updates the intrinsicContentSize. However the parent(contentView in tableview cell) ignores this new intrinsicContentSize.
So basically the result is that the subviews are layout and drawn correctly but the frame of the custom view is not updated/used in parent.
The question:
Is there a way to notify the parent about the change of the intrinsic size or a designated place to update the size calculated in layoutSubviews() so the new size is used in the parent?
Edit:
Here is the code in my custom view.
FYI: 8 is just the vertical and horizontal space between two subviews
class WrapView : UIView {
var height = CGFloat.zero
override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: height)
}
override func layoutSubviews() {
super.layoutSubviews()
guard frame.size.width != .zero else { return }
// Make subviews calc their size
subviews.forEach { $0.sizeToFit() }
// Check if there is enough space in a row to fit at least one view
guard subviews.map({ $0.frame.size.width }).max() ?? .zero <= frame.size.width else { return }
let width = frame.size.width
var row = [UIView]()
// rem is the remaining space in the current row
var rem = width
var y: CGFloat = .zero
var i = 0
while i < subviews.count {
let view = subviews[i]
let sizeNeeded = view.frame.size.width + (row.isEmpty ? 0 : 8)
let last = i == subviews.count - 1
let fit = rem >= sizeNeeded
if fit {
row.append(view)
rem -= sizeNeeded
i += 1
guard last else { continue }
}
let rowWidth = row.map { $0.frame.size.width + 8 }.reduce(-8, +)
var x = (width - rowWidth) * 0.5
for vw in row {
vw.frame.origin = CGPoint(x: x, y: y)
x += vw.frame.width + 8
}
y += row.map { $0.frame.size.height }.max()! + 8
rem = width
row = []
}
if height != y - 8 {
height = y - 8
invalidateIntrinsicContentSize()
}
}
}
After a lot of trying and research I finally solved the bug.
As #DonMag mentioned in the comments the new size of the cell wasn't recognized until a new layout pass. This could be verified by scrolling the cell off-screen and back in which showed the correct layout. Unfortunately it is harder than expected to trigger new pass as .beginUpdates() + .endUpdates()didn't
do the job.
Anyway I didn't find a way to trigger it but I followed the instructions described in this answer. Especially the part with the prototype cell for the height calculation provided a value which can be returned in tableview(heightForRowAt:).
Swift 5:
This is the code used for calculation:
let fitSize = CGSize(width: view.frame.size.width, height: .zero)
/* At this point populate the cell with the exact same data as the actual cell in the tableview */
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
cell.bounds = CGRect(x: .zero, y: .zero, width: view.frame.size.width, height: cell.bounds.height)
cell.setNeedsLayout()
cell.layoutIfNeeded()
height = headerCell.contentView.systemLayoutSizeFitting(fitSize).height + 1
The value is only calculated once and the cached as the size doesn't change anymore in my case.
Then the value can be returned in the delegate:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
indexPath.row == 0 ? height : UITableView.automaticDimension
}
I only used for the first cell as it is my header cell and there is only one section.
I try at the moment to resize the UIScrollView dynamically like in Android with wrap_content as attribute.
The data comes over a SQL Query that i start in the override func viewDidLoad()
I tried the freeform size variant and the version with extension
extension UIScrollView {
func resizeScrollViewContentSize() {
var contentRect = CGRect.zero
for view in self.subviews {
contentRect = contentRect.union(view.frame)
}
self.contentSize = contentRect.size
}
}
But nothing helps. Has Someone an idea?
EDIT:
#Stephen J I didn't understand your answer, sorry :(
I have a subview, too but how I can say the Scrollview that equals his size? I tried it with CTRL + Drag the mouse on the Scrollview and the same in the other direction.
So it looks at my Storyboard
The idea is OK, but...
Your extension is looping through the subviews of the Scroll view, but your Scroll view contains only ONE subview - your ContentView... and that view contains a bunch of subviews. (Actually, the Scroll view's subviews property also contains the scrollIndicator bars, but that doesn't really factor in here).
What you want to do is loop through the subviews of your ContentView, then change its size, and then set the .contentSize of the Scroll view based on the ContentView.
So...
// init empty rect
var contentRect = CGRect.zero
// for each subview in the ** Content ** view - NOT in the Scroll view
for view in theContentView.subviews {
// union the view's rect with the "total" rect
contentRect = contentRect.union(view.frame)
}
// set the frame size of the Content view to the "total" rect size
theContentView.frame.size = contentRect.size
// increast the size of the "total" rect by the Content view's x,y offsets
contentRect.size.width += theContentView.frame.origin.x
contentRect.size.height += theContentView.frame.origin.y
// NOW we can set the contentSize of the Scroll view
theScrollView.contentSize = contentRect.size
Personally, I've found using auto-layout and constraints to make working with Scroll views really, really easy - easier than this method, because you don't have to keep re-calculating. But, if this works for you, great :)
Edit: Here is a complete working example. It:
creates a ScrollView sized to the full view
adds a ContentView without size
adds 30 labels vertically with 16 pts spacing
then calculates and sets the size of the ContentView and the .contentSize of the Scroll view based on the ContentView's subviews
//
// ViewController.swift
// SWScrollNoAutoLayout
//
// Created by Don Mag on 6/14/17.
// Copyright © 2017 DonMag. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let theScrollView = UIScrollView()
let theContentView = UIView()
theScrollView.frame = self.view.bounds
theScrollView.backgroundColor = .orange
theContentView.backgroundColor = .yellow
self.view.addSubview(theScrollView)
theScrollView.addSubview(theContentView)
var yOffset: CGFloat = 0.0
for i in 1...30 {
let lbl = UILabel()
lbl.text = "Label: \(i)"
lbl.backgroundColor = .cyan
lbl.sizeToFit()
lbl.frame.origin.y = yOffset
theContentView.addSubview(lbl)
yOffset += lbl.frame.size.height + 16
}
// init empty rect
var contentRect = CGRect.zero
// for each subview in the ** Content ** view - NOT in the Scroll view
for view in theContentView.subviews {
// union the view's rect with the "total" rect
contentRect = contentRect.union(view.frame)
}
// set the frame size of the Content view to the "total" rect size
theContentView.frame.size = contentRect.size
// increast the size of the "total" rect by the Content view's x,y offsets
contentRect.size.width += theContentView.frame.origin.x
contentRect.size.height += theContentView.frame.origin.y
// NOW we can set the contentSize of the Scroll view
theScrollView.contentSize = contentRect.size
}
}
Is there possible way to adjust table view width by width of biggest text that should be set in the cellTitle label?
iOS 8 , SWIFT
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Set the popover tableview width to be max compressed size (assumes all cells visible)
let maxWidth = tableView.visibleCells.reduce(0.0) { (currentMax, cell) -> CGFloat in
return max(currentMax, cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).width)
}
var contentSize = tableView.contentSize
contentSize.width = maxWidth
tableView.contentSize = contentSize
tableView.bounds.size = contentSize
}
Set a width auto-layout constraint to the table view.
Set an outlet from the constraint to your code (ctrl+drag, just like a view).
When you have the size of the label (if the label also has auto-layout then you will have it at viewDidLayoutSubviews) set the width to the table like so:
- (void)viewDidLayoutSubviews {
self.myTableWidthConstraint.constant = self.myLabel.frame.size.width;
[self.myTableView updateConstraints];
[self.myTableView layoutIfNeeded];
}
You should be able to do that in the size inspector.
I'm been smashing my head against the wall with this for last 3 or 4 hours and I can't seem to figure it out. I have a UIViewController with a full screen UITableView inside of it (there's some other stuff on the screen, which is why I can't use a UITableViewController) and I want to get my tableHeaderView to resize with autolayout. Needless to say, it's not cooperating.
See screenshot below.
Because the overviewLabel (e.g. the "List overview information here." text) has dynamic content, I'm using autolayout to resize it and it's superview. I've got everything resizing nicely, except for the tableHeaderView, which is right below Paralax Table View in the hiearchy.
The only way I've found to resize that header view is programatically, with the following code:
CGRect headerFrame = self.headerView.frame;
headerFrame.size.height = headerFrameHeight;
self.headerView.frame = headerFrame;
[self.listTableView setTableHeaderView:self.headerView];
In this case, headerFrameHeight is a manual calculation of the tableViewHeader height as follows (innerHeaderView is the white area, or the second "View", headerView is tableHeaderView):
CGFloat startingY = self.innerHeaderView.frame.origin.y + self.overviewLabel.frame.origin.y;
CGRect overviewSize = [self.overviewLabel.text
boundingRectWithSize:CGSizeMake(290.f, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName: self.overviewLabel.font}
context:nil];
CGFloat overviewHeight = overviewSize.size.height;
CGFloat overviewPadding = ([self.overviewLabel.text length] > 0) ? 10 : 0; // If there's no overviewText, eliminate the padding in the overall height.
CGFloat headerFrameHeight = ceilf(startingY + overviewHeight + overviewPadding + 21.f + 10.f);
The manual calculation works, but it's clunky and prone to error if things change in the future. What I want to be able to do is have the tableHeaderView auto-resize based on the provided constraints, like you can anywhere else. But for the life of me, I can't figure it out.
There's several posts on SO about this, but none are clear and ended up confusing me more. Here's a few:
Auto layout on UITableViewHeader
Auto Layout for tableHeaderView
Is it possible to use AutoLayout with UITableView's tableHeaderView?
table header view height is wrong when using auto layout, IB, and font sizes
It doesn't really make sense to change the translatesAutoresizingMaskIntoConstraints property to NO, since that just causes errors for me and doesn't make sense conceptually anyway.
Any help would really be appreciated!
EDIT 1:
Thanks to TomSwift's suggestion, I was able to figure it out. Instead of manually calculating the height of the overview, I can have it calculated for me as follows and then re-set the tableHeaderView as before.
[self.headerView setNeedsLayout];
[self.headerView layoutIfNeeded];
CGFloat height = [self.innerHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + self.innerHeaderView.frame.origin.y; // adding the origin because innerHeaderView starts partway down headerView.
CGRect headerFrame = self.headerView.frame;
headerFrame.size.height = height;
self.headerView.frame = headerFrame;
[self.listTableView setTableHeaderView:self.headerView];
Edit 2: As others have noted, the solution posted in Edit 1 doesn't seem to work in viewDidLoad. It does, however, seem to work in viewWillLayoutSubviews. Example code below:
// Note 1: The variable names below don't match the variables above - this is intended to be a simplified "final" answer.
// Note 2: _headerView was previously assigned to tableViewHeader (in loadView in my case since I now do everything programatically).
// Note 3: autoLayout code can be setup programatically in updateViewConstraints.
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[_headerWrapper setNeedsLayout];
[_headerWrapper layoutIfNeeded];
CGFloat height = [_headerWrapper systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGRect headerFrame = _headerWrapper.frame;
headerFrame.size.height = height;
_headerWrapper.frame = headerFrame;
_tableView.tableHeaderView = _headerWrapper;
}
You need to use the UIView systemLayoutSizeFittingSize: method to obtain the minimum bounding size of your header view.
I provide further discussion on using this API in this Q/A:
How to resize superview to fit all subviews with autolayout?
I've found an elegant way to way to use auto layout to resize table headers, with and without animation.
Simply add this to your View Controller.
func sizeHeaderToFit(tableView: UITableView) {
if let headerView = tableView.tableHeaderView {
let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
var frame = headerView.frame
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
}
}
To resize according to a dynamically changing label:
#IBAction func addMoreText(sender: AnyObject) {
self.label.text = self.label.text! + "\nThis header can dynamically resize according to its contents."
}
override func viewDidLayoutSubviews() {
// viewDidLayoutSubviews is called when labels change.
super.viewDidLayoutSubviews()
sizeHeaderToFit(tableView)
}
To animate a resize according to a changes in a constraint:
#IBOutlet weak var makeThisTallerHeight: NSLayoutConstraint!
#IBAction func makeThisTaller(sender: AnyObject) {
UIView.animateWithDuration(0.3) {
self.tableView.beginUpdates()
self.makeThisTallerHeight.constant += 20
self.sizeHeaderToFit(self.tableView)
self.tableView.endUpdates()
}
}
See the AutoResizingHeader project to see this in action.
https://github.com/p-sun/Swift2-iOS9-UI
I really battled with this one and plonking the setup into viewDidLoad didn't work for me since the frame is not set in viewDidLoad, I also ended up with tons of messy warnings where the encapsulated auto layout height of the header was being reduced to 0. I only noticed the issue on iPad when presenting a tableView in a Form presentation.
What solved the issue for me was setting the tableViewHeader in viewWillLayoutSubviews rather than in viewDidLoad.
func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if tableView.tableViewHeaderView == nil {
let header: MyHeaderView = MyHeaderView.createHeaderView()
header.setNeedsUpdateConstraints()
header.updateConstraintsIfNeeded()
header.frame = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), CGFloat.max)
var newFrame = header.frame
header.setNeedsLayout()
header.layoutIfNeeded()
let newSize = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
newFrame.size.height = newSize.height
header.frame = newFrame
self.tableView.tableHeaderView = header
}
}
This solution resizes the tableHeaderView and avoids infinite loop in the viewDidLayoutSubviews() method I was having with some of the other answers here:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let headerView = tableView.tableHeaderView {
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
var headerFrame = headerView.frame
// comparison necessary to avoid infinite loop
if height != headerFrame.size.height {
headerFrame.size.height = height
headerView.frame = headerFrame
tableView.tableHeaderView = headerView
}
}
}
See also this post: https://stackoverflow.com/a/34689293/1245231
Your solution using systemLayoutSizeFittingSize: works if the header view is just updated once on each view appearance. In my case, the header view updated multiple times to reflect status changes. But systemLayoutSizeFittingSize: always reported the same size. That is, the size corresponding to the first update.
To get systemLayoutSizeFittingSize: to return the correct size after each update I had to first remove the table header view before updating it and re-adding it:
self.listTableView.tableHeaderView = nil;
[self.headerView removeFromSuperview];
This worked for me on ios10 and Xcode 8
func layoutTableHeaderView() {
guard let headerView = tableView.tableHeaderView else { return }
headerView.translatesAutoresizingMaskIntoConstraints = false
let headerWidth = headerView.bounds.size.width;
let temporaryWidthConstraints = NSLayoutConstraint.constraintsWithVisualFormat("[headerView(width)]", options: NSLayoutFormatOptions(rawValue: UInt(0)), metrics: ["width": headerWidth], views: ["headerView": headerView])
headerView.addConstraints(temporaryWidthConstraints)
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let headerSize = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
let height = headerSize.height
var frame = headerView.frame
frame.size.height = height
headerView.frame = frame
self.tableView.tableHeaderView = headerView
headerView.removeConstraints(temporaryWidthConstraints)
headerView.translatesAutoresizingMaskIntoConstraints = true
}
It works for both header view and footer just replace the header with footer
func sizeHeaderToFit() {
if let headerView = tableView.tableHeaderView {
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
var frame = headerView.frame
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
For iOS 12 and above, the following steps will ensure autolayout works properly in both your table and the header.
Create your tableView first, then the header.
At the end of your header creation code, call:
[headerV setNeedsLayout];
[headerV layoutIfNeeded];
Upon orientation change, mark the header for layout again and reload the table, this needs to happen slightly after the orientation change is reported:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 *NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[tableV.tableHeaderView setNeedsLayout];
[tableV.tableHeaderView layoutIfNeeded];
[tableV reloadData];});
In my case viewDidLayoutSubviews worked better. viewWillLayoutSubviews causes white lines of a tableView to appear. Also I added checking if my headerView object already exists.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if ( ! self.userHeaderView ) {
// Setup HeaderView
self.userHeaderView = [[[NSBundle mainBundle] loadNibNamed:#"SSUserHeaderView" owner:self options:nil] objectAtIndex:0];
[self.userHeaderView setNeedsLayout];
[self.userHeaderView layoutIfNeeded];
CGFloat height = [self.userHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGRect headerFrame = self.userHeaderView.frame;
headerFrame.size.height = height;
self.userHeaderView.frame = headerFrame;
self.tableView.tableHeaderView = self.userHeaderView;
// Update HeaderView with data
[self.userHeaderView updateWithProfileData];
}
}
It is quite possible to use generic AutoLayout-based UIView with any AL inner subview structure as a tableHeaderView.
The only thing one needs is to set a simple tableFooterView before!
Let self.headerView is some constraint-based UIView.
- (void)viewDidLoad {
........................
self.tableView.tableFooterView = [UIView new];
[self.headerView layoutIfNeeded]; // to set initial size
self.tableView.tableHeaderView = self.headerView;
[self.tableView.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor].active = YES;
[self.tableView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;
[self.tableView.topAnchor constraintEqualToAnchor:self.headerView.topAnchor].active = YES;
// and the key constraint
[self.tableFooterView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;
}
If self.headerView changes height under UI rotation one have to implement
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition: ^(id<UIViewControllerTransitionCoordinatorContext> context) {
// needed to resize header height
self.tableView.tableHeaderView = self.headerView;
} completion: NULL];
}
One can use ObjC category for this purpose
#interface UITableView (AMHeaderView)
- (void)am_insertHeaderView:(UIView *)headerView;
#end
#implementation UITableView (AMHeaderView)
- (void)am_insertHeaderView:(UIView *)headerView {
NSAssert(self.tableFooterView, #"Need to define tableFooterView first!");
[headerView layoutIfNeeded];
self.tableHeaderView = headerView;
[self.leadingAnchor constraintEqualToAnchor:headerView.leadingAnchor].active = YES;
[self.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor].active = YES;
[self.topAnchor constraintEqualToAnchor:headerView.topAnchor].active = YES;
[self.tableFooterView.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor].active = YES;
}
#end
And also Swift extension
extension UITableView {
func am_insertHeaderView2(_ headerView: UIView) {
assert(tableFooterView != nil, "Need to define tableFooterView first!")
headerView.layoutIfNeeded()
tableHeaderView = headerView
leadingAnchor.constraint(equalTo: headerView.leadingAnchor).isActive = true
trailingAnchor.constraint(equalTo: headerView.trailingAnchor).isActive = true
topAnchor.constraint(equalTo: headerView.topAnchor).isActive = true
tableFooterView?.trailingAnchor.constraint(equalTo: headerView.trailingAnchor).isActive = true
}
}
This solution works perfectly for me:
https://spin.atomicobject.com/2017/08/11/swift-extending-uitableviewcontroller/
It extends the UITableViewController. But if you are just using a UITableView, it will still work, just extend the UITableView instead of the UITableViewController.
Call the methods sizeHeaderToFit() or sizeFooterToFit() whenever there is an event that changes the tableViewHeader height.
Copied from this post
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let headerView = tableView.tableHeaderView {
let height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
var headerFrame = headerView.frame
//Comparison necessary to avoid infinite loop
if height != headerFrame.size.height {
headerFrame.size.height = height
headerView.frame = headerFrame
tableView.tableHeaderView = headerView
}
}
}