I have a UIView
subclass that contains a multi-line UILabel
. This view uses autolayout.
I would like to set this view as the tableHeaderView
of a UITableView
(not a section header). The height of this header will depend on the text of the label, which in turn depends on the width of the device. The sort of scenario autolayout should be great at.
I have found and attempted many many solutions to get this working, but to no avail. Some of the things I've tried:
preferredMaxLayoutWidth
on each label during layoutSubviews
intrinsicContentSize
tableHeaderView
's frame manually.Some of the various failures I've encountered:
Auto Layout still required after executing -layoutSubviews
The solution (or solutions, if necessary) should work for both iOS 7 and iOS 8. Note that all of this is being done programmatically. I've set up a small sample project in case you want to hack on it to see the issue. I've reset my efforts to the following start point:
SCAMessageView *header = [[SCAMessageView alloc] init];
header.titleLabel.text = @"Warning";
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.";
self.tableView.tableHeaderView = header;
What am I missing?
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 tableHeaderView
s, 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);
}
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)
For anyone still looking for a solution, this is for Swift 3 & iOS 9+. Here is one using only AutoLayout. It also updates correctly on device rotation.
extension UITableView {
// 1.
func setTableHeaderView(headerView: UIView) {
headerView.translatesAutoresizingMaskIntoConstraints = false
self.tableHeaderView = headerView
// ** Must setup AutoLayout after set tableHeaderView.
headerView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
headerView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
headerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
}
// 2.
func shouldUpdateHeaderViewFrame() -> Bool {
guard let headerView = self.tableHeaderView else { return false }
let oldSize = headerView.bounds.size
// Update the size
headerView.layoutIfNeeded()
let newSize = headerView.bounds.size
return oldSize != newSize
}
}
To use:
override func viewDidLoad() {
...
// 1.
self.tableView.setTableHeaderView(headerView: customView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 2. Reflect the latest size in tableHeaderView
if self.tableView.shouldUpdateHeaderViewFrame() {
// **This is where table view's content (tableHeaderView, section headers, cells)
// frames are updated to account for the new table header size.
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
}
The gist is that you should let tableView
manage the frame of tableHeaderView
the same way as table view cells. This is done through tableView
's beginUpdates/endUpdates
.
The thing is that tableView
doesn't care about AutoLayout when it updates the children frames. It uses the current tableHeaderView
's size to determine where the first cell/section header should be.
1) Add a width constraint so that the tableHeaderView
uses this width whenever we call layoutIfNeeded(). Also add centerX and top constraints to position it correctly relative to the tableView
.
2) To let the tableView
knows about the latest size of tableHeaderView
, e.g., when the device is rotated, in viewDidLayoutSubviews we can call layoutIfNeeded() on tableHeaderView
. Then, if the size is changed, call beginUpdates/endUpdates.
Note that I don't include beginUpdates/endUpdates in one function, as we might want to defer the call to later.
Check out a sample project
The following UITableView
extension solves all common problems of autolayouting and positioning of the tableHeaderView
without frame-use legacy:
@implementation UITableView (AMHeaderView)
- (void)am_insertHeaderView:(UIView *)headerView
{
self.tableHeaderView = headerView;
NSLayoutConstraint *constraint =
[NSLayoutConstraint constraintWithItem: headerView
attribute: NSLayoutAttributeWidth
relatedBy: NSLayoutRelationEqual
toItem: headerView.superview
attribute: NSLayoutAttributeWidth
multiplier: 1.0
constant: 0.0];
[headerView.superview addConstraint:constraint];
[headerView layoutIfNeeded];
NSArray *constraints = headerView.constraints;
[headerView removeConstraints:constraints];
UIView *layoutView = [UIView new];
layoutView.translatesAutoresizingMaskIntoConstraints = NO;
[headerView insertSubview:layoutView atIndex:0];
[headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]];
[headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]];
[headerView addConstraints:constraints];
self.tableHeaderView = headerView;
[headerView layoutIfNeeded];
}
@end
Explanation of the "strange" steps:
At first we tie the headerView width to the tableView width: it helps as under rotations and prevent from deep left shift of X-centered subviews of the headerView.
(the Magic!) We insert fake layoutView in the headerView:
At this moment we STRONGLY need to remove all headerView constraints,
expand the layoutView to the headerView and then restore initial headerView
constraints. It happens that order of constraints has some sense!
In the way we get correct headerView height auto calculation and also correct
X-centralization for all headerView subviews.
Then we only need to re-layout headerView again to obtain correct tableView
height calculation and headerView positioning above sections without
intersecting.
P.S. It works for iOS8 also. It is impossible to comment out any code string here in common case.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With