How to Set the Height of Tableheaderview (Uitableview) with Autolayout

Setting tableHeaderView height dynamically

Copied from this post. (Make sure you see it if you're looking for more details)

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
}
}
}

How do I set the height of tableHeaderView (UITableView) with autolayout?

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?

UITableView tableHeaderView using autolayout height too small

Answering my own question here:

There are 2 steps that seem to get this to work. The first is in ViewDidLoad, and the second is in viewDidLayoutSubviews:

var headerView: UIView!

override func viewDidLoad() {
super.viewDidLoad()

if let currentTableHeaderView = self.tableView.tableHeaderView {
currentTableHeaderView.removeFromSuperview()
}
// Setting the table header view with a height of 0.01 fixes a bug that adds a gap between the
// tableHeaderView (once added) and the top row. See: http://stackoverflow.com/a/18938763/657676
self.tableView.tableHeaderView = UIView(frame: CGRectMake(0, 0, CGRectGetWidth(self.tableView.frame), 0.01))
self.headerView = AboutTableViewHeaderView(frame: CGRectZero)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let tableHeaderView = self.headerView {
var frame = CGRectZero
frame.size.width = self.tableView.bounds.size.width
frame.size.height = tableHeaderView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
if self.tableView.tableHeaderView == nil || !CGRectEqualToRect(frame, tableHeaderView.frame) {
tableHeaderView.frame = frame
tableHeaderView.layoutIfNeeded()
self.tableView.tableHeaderView = tableHeaderView
}
}
}

Hopefully that all makes sense. The viewDidLayoutSubview code is via http://osdir.com/ml/general/2014-06/msg19399.html

UITableView: Set table header view height based on screen size

Unfortunately, table header views cannot be sized using auto layout. You can use auto layout for elements inside the header but you have to specify the header's size by explicitly setting its frame. If the header's height is static and known at compile time you can use IB. However, if the height is dynamic or depends on the device (as in your case), you have to set it in code.

A quite flexible solution would be to create a custom subclass of UITableView and adapt the header's frame in the layoutSubviews method. This way the header's size gets automatically adjusted when the table view is resized. You have to be careful, however, to only re-apply the header's frame when a change is actually needed to avoid an infinite loop.

Here's what it would look like in Objective-C:

@interface MyTableView : UITableView
@end

@implementation MyTableView : UITableView

- (void)layoutSubviews {
[super layoutSubviews];

if (self.tableHeaderView) {
UIView *header = self.tableHeaderView;
CGRect rect = CGRectMake(0, 0, self.bounds.size.width,
self.bounds.size.height / 2);

// Only adjust frame if needed to avoid infinite loop
if (!CGRectEqualToRect(self.tableHeaderView.frame, rect)) {
header.frame = rect;

// This will apply the new header size and trigger another
// call of layoutSubviews
self.tableHeaderView = header;
}
}
}

@end

The Swift version looks like this:

class MyTableView: UITableView {

override func layoutSubviews() {
super.layoutSubviews()

if let header = tableHeaderView {
let rect = CGRectMake(0, 0, bounds.size.width, bounds.size.height / 2)

// Only adjust frame if needed to avoid infinite loop
if !CGRectEqualToRect(header.frame, rect) {
header.frame = rect

// This will apply the new header size and trigger
// another call of layoutSubviews
tableHeaderView = header
}
}
}

}

Note that the above snippets use the bounds of the table view rather than the screen size to calculate the header size.

Update: Note that sometimes an additional call to layoutIfNeeded is needed after setting the tableHeaderView property. I ran into an issue where section headers were drawn above the header view without calling layoutIfNeeded.

Is it possible to use AutoLayout with UITableView's tableHeaderView?

I asked and answered a similar question here. In summary, I add the header once and use it to find the required height. That height can then be applied to the header, and the header is set a second time to reflect the change.

- (void)viewDidLoad
{
[super viewDidLoad];

self.header = [[SCAMessageView alloc] init];
self.header.titleLabel.text = @"Warning";
self.header.subtitleLabel.text = @"This is a message with enough text to span multiple lines. This text is set at runtime and might be short or long.";

//set the tableHeaderView so that the required height can be determined
self.tableView.tableHeaderView = self.header;
[self.header setNeedsLayout];
[self.header layoutIfNeeded];
CGFloat height = [self.header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

//update the header's frame and set it again
CGRect headerFrame = self.header.frame;
headerFrame.size.height = height;
self.header.frame = headerFrame;
self.tableView.tableHeaderView = self.header;
}

If you have multi-line labels, this also relies on the custom view setting the preferredMaxLayoutWidth of each label:

- (void)layoutSubviews
{
[super layoutSubviews];

self.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.titleLabel.frame);
self.subtitleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.subtitleLabel.frame);
}

or perhaps more generally:

override func layoutSubviews() {
super.layoutSubviews()
for view in subviews {
guard let label = view as? UILabel where label.numberOfLines == 0 else { continue }
label.preferredMaxLayoutWidth = CGRectGetWidth(label.frame)
}
}

Update January 2015

Unfortunately this still seems necessary. Here is a swift version of the layout process:

tableView.tableHeaderView = header
header.setNeedsLayout()
header.layoutIfNeeded()
header.frame.size = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
tableView.tableHeaderView = header

I've found it useful to move this into an extension on UITableView:

extension UITableView {
//set the tableHeaderView so that the required height can be determined, update the header's frame and set it again
func setAndLayoutTableHeaderView(header: UIView) {
self.tableHeaderView = header
self.tableHeaderView?.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
header.widthAnchor.constraint(equalTo: self.widthAnchor)
])
header.setNeedsLayout()
header.layoutIfNeeded()
header.frame.size = header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
self.tableHeaderView = header
}
}

Usage:

let header = SCAMessageView()
header.titleLabel.text = "Warning"
header.subtitleLabel.text = "Warning message here."
tableView.setAndLayoutTableHeaderView(header)

UITableView header dynamic height in run-time

This is how i have approached it

Using tableview
I have created the UI For the header in XIB

Now in the following delegate method

func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {

}

I Create a UIView for the header and calculate the height based on the content and return the same.

Now i can return the same header view from the following delegate method

func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

}

Based on the section i again create a view from xib and return that view from the method.

In my case i needed only one headerview for table so i kept 2 sections and returned the headerview for section one.

Using autolayout in a tableHeaderView

My own best answer so far involves setting the tableHeaderView once and forcing a layout pass. This allows a required size to be measured, which I then use to set the frame of the header. And, as is common with tableHeaderViews, I have to again set it a second time to apply the change.

- (void)viewDidLoad
{
[super viewDidLoad];

self.header = [[SCAMessageView alloc] init];
self.header.titleLabel.text = @"Warning";
self.header.subtitleLabel.text = @"This is a message with enough text to span multiple lines. This text is set at runtime and might be short or long.";

//set the tableHeaderView so that the required height can be determined
self.tableView.tableHeaderView = self.header;
[self.header setNeedsLayout];
[self.header layoutIfNeeded];
CGFloat height = [self.header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

//update the header's frame and set it again
CGRect headerFrame = self.header.frame;
headerFrame.size.height = height;
self.header.frame = headerFrame;
self.tableView.tableHeaderView = self.header;
}

For multiline labels, this also relies on the custom view (the message view in this case) setting the preferredMaxLayoutWidth of each:

- (void)layoutSubviews
{
[super layoutSubviews];

self.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.titleLabel.frame);
self.subtitleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.subtitleLabel.frame);
}

Update January 2015

Unfortunately this still seems necessary. Here is a swift version of the layout process:

tableView.tableHeaderView = header
header.setNeedsLayout()
header.layoutIfNeeded()
let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
var frame = header.frame
frame.size.height = height
header.frame = frame
tableView.tableHeaderView = header

I've found it useful to move this into an extension on UITableView:

extension UITableView {
//set the tableHeaderView so that the required height can be determined, update the header's frame and set it again
func setAndLayoutTableHeaderView(header: UIView) {
self.tableHeaderView = header
header.setNeedsLayout()
header.layoutIfNeeded()
let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
var frame = header.frame
frame.size.height = height
header.frame = frame
self.tableHeaderView = header
}
}

Usage:

let header = SCAMessageView()
header.titleLabel.text = "Warning"
header.subtitleLabel.text = "Warning message here."
tableView.setAndLayoutTableHeaderView(header)

Calculate tableHeaderView height with autolayout

I think you are trying too hard to fit you content into the tableview header, which was designed to contain relatively small static content.

Assuming what you want is your tableview to stay at the bottom of the orange view, and the orange view to scroll with the tableview (ie. not stay at the top of the screen at all time), I would add a scroll view parent of both the orange view and the tableview, and disable the tableview scroll capability.

This means however to exchange your UITableViewController for a UIViewController.

eg.

-- UIScrollView
|
-- UIView (orange view)
| |
| -- subview 1
| -- subview 2
| -- etc.
|
-- UITableView (scrollEnabled = NO)

With AutoLayout, you can make the orange view resize automatically to fit its content, which will "push" the UITableView down.
When the user swipe vertically, the root scrollview will catch the events, and scroll the whole (orangeView + tableView) accordingly, resulting in the same effect as if the orange view was the table view header.



Related Topics



Leave a reply



Submit