I have a UIView
subclass that contains a multi-line UILabel
. This view uses autolayout.
Some of the answers here helped me get very close to what I needed. But I encountered conflicts with the constraint "UIView-Encapsulated-Layout-Width" which is set by the system, when rotating the device back-and-forth between portrait and landscape. My solution below is largely based on this gist by marcoarment (credit to him): https://gist.github.com/marcoarment/1105553afba6b4900c10. The solution does not rely on the header view containing a UILabel. There are 3 parts:
func rr_layoutTableHeaderView(width:CGFloat) {
// remove headerView from tableHeaderView:
guard let headerView = self.tableHeaderView else { return }
headerView.removeFromSuperview()
self.tableHeaderView = nil
// create new superview for headerView (so that autolayout can work):
let temporaryContainer = UIView(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
temporaryContainer.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(temporaryContainer)
temporaryContainer.addSubview(headerView)
// set width constraint on the headerView and calculate the right size (in particular the height):
headerView.translatesAutoresizingMaskIntoConstraints = false
let temporaryWidthConstraint = NSLayoutConstraint(item: headerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: width)
temporaryWidthConstraint.priority = 999 // necessary to avoid conflict with "UIView-Encapsulated-Layout-Width"
headerView.addConstraint(temporaryWidthConstraint)
headerView.frame.size = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
// remove the temporary constraint:
headerView.removeConstraint(temporaryWidthConstraint)
headerView.translatesAutoresizingMaskIntoConstraints = true
// put the headerView back into the tableHeaderView:
headerView.removeFromSuperview()
temporaryContainer.removeFromSuperview()
self.tableHeaderView = headerView
}
override func viewDidLoad() {
super.viewDidLoad()
// build the header view using autolayout:
let button = UIButton()
let label = UILabel()
button.setTitle("Tap here", for: .normal)
label.text = "The text in this header will span multiple lines if necessary"
label.numberOfLines = 0
let headerView = UIStackView(arrangedSubviews: [button, label])
headerView.axis = .horizontal
// assign the header view:
self.tableView.tableHeaderView = headerView
// continue with other things...
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tableView.rr_layoutTableHeaderView(width: view.frame.width)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
self.tableView.rr_layoutTableHeaderView(width: size.width)
}
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)
Using Extension in Swift 3.0
extension UITableView {
func setTableHeaderView(headerView: UIView?) {
// set the headerView
tableHeaderView = headerView
// check if the passed view is nil
guard let headerView = headerView else { return }
// check if the tableHeaderView superview view is nil just to avoid
// to use the force unwrapping later. In case it fail something really
// wrong happened
guard let tableHeaderViewSuperview = tableHeaderView?.superview else {
assertionFailure("This should not be reached!")
return
}
// force updated layout
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
// set tableHeaderView width
tableHeaderViewSuperview.addConstraint(headerView.widthAnchor.constraint(equalTo: tableHeaderViewSuperview.widthAnchor, multiplier: 1.0))
// set tableHeaderView height
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
tableHeaderViewSuperview.addConstraint(headerView.heightAnchor.constraint(equalToConstant: height))
}
func setTableFooterView(footerView: UIView?) {
// set the footerView
tableFooterView = footerView
// check if the passed view is nil
guard let footerView = footerView else { return }
// check if the tableFooterView superview view is nil just to avoid
// to use the force unwrapping later. In case it fail something really
// wrong happened
guard let tableFooterViewSuperview = tableFooterView?.superview else {
assertionFailure("This should not be reached!")
return
}
// force updated layout
footerView.setNeedsLayout()
footerView.layoutIfNeeded()
// set tableFooterView width
tableFooterViewSuperview.addConstraint(footerView.widthAnchor.constraint(equalTo: tableFooterViewSuperview.widthAnchor, multiplier: 1.0))
// set tableFooterView height
let height = footerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
tableFooterViewSuperview.addConstraint(footerView.heightAnchor.constraint(equalToConstant: height))
}
}
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.
This should do the trick for a headerView or a footerView for the UITableView using AutoLayout.
extension UITableView {
var tableHeaderViewWithAutolayout: UIView? {
set (view) {
tableHeaderView = view
if let view = view {
lowerPriorities(view)
view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
tableHeaderView = view
}
}
get {
return tableHeaderView
}
}
var tableFooterViewWithAutolayout: UIView? {
set (view) {
tableFooterView = view
if let view = view {
lowerPriorities(view)
view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
tableFooterView = view
}
}
get {
return tableFooterView
}
}
fileprivate func lowerPriorities(_ view: UIView) {
for cons in view.constraints {
if cons.priority.rawValue == 1000 {
cons.priority = UILayoutPriority(rawValue: 999)
}
for v in view.subviews {
lowerPriorities(v)
}
}
}
}
Your constraints were just a little off. Take a look at this and let me know if you have any questions. For some reason I had difficulty getting the background of the view to stay red? So I created a filler view that fills the gap created by having a titleLabel
and subtitleLabel
height that is greater than the height of the imageView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.backgroundColor = [UIColor redColor];
self.imageView = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"Exclamation"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
self.imageView.tintColor = [UIColor whiteColor];
self.imageView.translatesAutoresizingMaskIntoConstraints = NO;
self.imageView.backgroundColor = [UIColor redColor];
[self addSubview:self.imageView];
[self.imageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self);
make.width.height.equalTo(@40);
make.top.equalTo(self).offset(0);
}];
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.titleLabel.font = [UIFont systemFontOfSize:14];
self.titleLabel.textColor = [UIColor whiteColor];
self.titleLabel.backgroundColor = [UIColor redColor];
[self addSubview:self.titleLabel];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self).offset(0);
make.left.equalTo(self.imageView.mas_right).offset(0);
make.right.equalTo(self).offset(-10);
make.height.equalTo(@15);
}];
self.subtitleLabel = [[UILabel alloc] init];
self.subtitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.subtitleLabel.font = [UIFont systemFontOfSize:13];
self.subtitleLabel.textColor = [UIColor whiteColor];
self.subtitleLabel.numberOfLines = 0;
self.subtitleLabel.backgroundColor = [UIColor redColor];
[self addSubview:self.subtitleLabel];
[self.subtitleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.titleLabel.mas_bottom);
make.left.equalTo(self.imageView.mas_right);
make.right.equalTo(self).offset(-10);
}];
UIView *fillerView = [[UIView alloc] init];
fillerView.backgroundColor = [UIColor redColor];
[self addSubview:fillerView];
[fillerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.imageView.mas_bottom);
make.bottom.equalTo(self.subtitleLabel.mas_bottom);
make.left.equalTo(self);
make.right.equalTo(self.subtitleLabel.mas_left);
}];
}
return self;
}