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
}
}
}
Related
I'm trying to create a tableview programmatically that has a search bar in the tableHeaderView. For some reason the search bar appears on top of the first cell.
I'm using Masonry to build constraints.
Can someone point me to what i'm doing wrong.
- (void)setupViews {
...
self.tableView = [[UITableView alloc] initWithFrame:CGRectZero];
[self.view addSubview:self.tableView];
self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectZero];
self.tableView.tableHeaderView = self.searchBar;
...
}
- (void)updateViewConstraints {
[self.searchBar mas_updateConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(self.view);
make.height.equalTo(#(44));
}];
[self.tableView mas_updateConstraints:^(MASConstraintMaker *make) {
make.self.top.equalTo(self.view);
make.self.bottom.equalTo(self.toolbar.mas_top);
make.width.equalTo(self.view);
}];
...
}
You can see here that the header is at the same level as the cells.
Thanks for your help, I found a gist on GitHub which talked about changing the size of tableViewHeader using AutoLayout:
https://gist.github.com/andreacremaschi/833829c80367d751cb83
- (void) sizeHeaderToFit {
UIView *headerView = self.tableHeaderView;
[headerView setNeedsLayout];
[headerView layoutIfNeeded];
CGFloat height = [headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
headerView.frame = ({
CGRect headerFrame = headerView.frame;
headerFrame.size.height = height;
headerFrame;
});
self.tableHeaderView = headerView;
}
If I call this method during updateViewConstraints then it works.
However, I don't fully understand it.
Using Extension in Swift 3.0
extension UITableView {
func setTableHeaderView(headerView: UIView?) {
// prepare the headerView for constraints
headerView?.translatesAutoresizingMaskIntoConstraints = false
// 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?) {
// prepare the headerView for constraints
headerView?.translatesAutoresizingMaskIntoConstraints = false
// 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))
}
}
I think the problem is because you are using autolayout and setting frames to views manually, replace this code:
self.tableView = [[UITableView alloc] initWithFrame:CGRectZero];
[self.view addSubview:self.tableView];
self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectZero];
self.tableView.tableHeaderView = self.searchBar;
with something like this:
self.tableView = [UITableView new];
self.tableView.translatesAutoresizingMasksIntoConstraints = NO;
[self.view addSubview:self.tableView];
self.searchBar = [UISearchBar new];
self.searchBar.translatesAutoresizingMasksIntoConstraints = NO;
self.tableView.tableHeaderView = self.searchBar;
It may not work, because maybe searchBar's frame needs to be set manually without constraints.
Here issue regarding space between table view Header and table view Cell. You can handle using Attribute Inspector. Please review that.
- select UITableView
- Under attribute inspector -> Scroll view size -> Content insets, set Top = 44 (or whichever is your nav bar height).
Or you can Handle it programmatically.
- (void)viewDidLoad {
UIEdgeInsets inset = UIEdgeInsetsMake(20, 0, 0, 0);
self.tableView.contentInset = inset;
}
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:
setting a preferredMaxLayoutWidth on each label during layoutSubviews
defining an intrinsicContentSize
attempting to figure out the required size for the view and setting the tableHeaderView's frame manually.
adding a width constraint to the view when the header is set
a bunch of other things
Some of the various failures I've encountered:
label extends beyond the width of the view, doesn't wrap
frame's height is 0
app crashes with exception 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 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)
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.
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:
A function defined in an extension to UITableView.
Call the function from the view controller's viewWillAppear().
Call the function from the view controller's viewWillTransition() in order to handle device rotation.
UITableView extension
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
}
Use in UITableViewController
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)
}
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)
}
}
}
}
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))
}
}
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;
}
I'll add my 2 cents since this question is highly indexed in Google. I think you should be using
self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension
self.tableView.estimatedSectionHeaderHeight = 200 //a rough estimate, doesn't need to be accurate
in your ViewDidLoad. Also, to load a custom UIView to a Header you should really be using viewForHeaderInSection delegate method. You can have a custom Nib file for your header (UIView nib). That Nib must have a controller class which subclasses UITableViewHeaderFooterView like-
class YourCustomHeader: UITableViewHeaderFooterView {
//#IBOutlets, delegation and other methods as per your needs
}
Make sure your Nib file name is the same as the class name just so you don't get confused and it's easier to manage. like YourCustomHeader.xib and YourCustomHeader.swift (containing class YourCustomHeader). Then, just assign YourCustomHeader to your Nib file using identity inspector in the interface builder.
Then register the Nib file as your header view in the main View Controller's viewDidLoad like-
tableView.register(UINib(nibName: "YourCustomHeader", bundle: nil), forHeaderFooterViewReuseIdentifier: "YourCustomHeader")
And then in your heightForHeaderInSection just return UITableViewAutomaticDimension. This is how the delegates should look like-
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "YourCustomHeader") as! YourCustomHeader
return headerView
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableViewAutomaticDimension
}
This is a much simpler and the appropriate way of doing without the "Hackish" ways suggested in the accepted answer since multiple forced layouts could impact your app's performance, especially if you have multiple custom headers in your tableview. Once you do the above method as I suggest, you would notice your Header (and or Footer) view expand and shrink magically based on your custom view's content size (provided you are using AutoLayout in the custom view, i.e. YourCustomHeader, nib file).
I'm trying to create a UITableView with a header view using autolayout using storyboards. It looks fine in Xcode, but when I run the app, it does not look the same.
In Xcode:
In the app:
The image has a constraint for 150x150, and there are 8-high constraints between the top-image, image-middle label, middle description-label and description label-bottom.
Both labels have numberOfRows set to 0 and lineBreakMode set to ByWordWrapping.
I have tried settings the frame via:
if let headerView = self.tableView.tableHeaderView {
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
println("Setting height to \(height)")
var headerFrame = headerView.frame
headerFrame.size.height = height
headerView.frame = headerFrame
self.tableView.tableHeaderView = headerView
}
One of the original issues was that I had some erroneous constraints (that for some reason Xcode only started complaining about today), so I have removed those and set a contentHuggingPriority of 252 (higher than all others) on the app name label. When I resize the header view manually in the storyboard the image and app name label stay the same height, and the description label grows. It would appear that the apps uses the size of the header in the storyboard at run time, and doesn't get the height from its children.
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
There are numerous threads about correctly sizing a tableHeaderView with auto-layout (one such thread) but they tend to pre-date iOS 8.
I have a situation with numerous table views, all with headers, that size correctly under iOS 7 but incorrectly under iOS 8 using the code that most of the aforementioned threads champion. In the controllers for the tables, I have the following method:
- (void)rejiggerTableHeaderView
{
self.tableView.tableHeaderView = nil;
UIView *header = self.headerView;
[header setNeedsLayout];
[header layoutIfNeeded];
CGFloat height = [header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGRect headerFrame = header.frame;
headerFrame.size.height = height;
header.frame = headerFrame;
self.tableView.tableHeaderView = header;
}
With a multi-line label under iOS 7, this correctly sizes the table view's header like so:
But the same code run under iOS 8 produces the following:
What's the trick to getting -systemLayoutSizeFittingSize: to return the correct size under iOS 8? Here's a sample project that demonstrates the issue.
Changing your headerview function to the following works for me:
- (void)rejiggerTableHeaderView
{
self.tableView.tableHeaderView = nil;
UIView *header = self.headerView;
CGRect frame = header.frame;
frame.size.width = self.tableView.frame.size.width;
header.frame = frame;
[header setNeedsLayout];
[header layoutIfNeeded];
CGFloat height = [header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGRect headerFrame = header.frame;
headerFrame.size.height = height;
header.frame = headerFrame;
self.tableView.tableHeaderView = header;
}
Problem can be in non-set preferredMaxLayoutWidth.
If you will set it to correct UILabel width, it will determine constraints correctly.
You can go through all UILabel in header and set preferredMaxLayoutWidth to label width.
Swift 3 example:
extension UITableView {
public func relayoutTableHeaderView() {
if let tableHeaderView = tableHeaderView {
let labels = tableHeaderView.findViewsOfClass(viewClass: UILabel.self)
for label in labels {
label.preferredMaxLayoutWidth = label.frame.width
}
tableHeaderView.setNeedsLayout()
tableHeaderView.layoutIfNeeded()
tableHeaderView.frame.height = tableHeaderView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
self.tableHeaderView = tableHeaderView
}
}
public func findViewsOfClass<T:UIView>(viewClass: T.Type) -> [T] {
var views: [T] = []
for subview in subviews {
if subview is T {
views.append(subview as! T)
}
views.append(contentsOf: subview.findViewsOfClass(viewClass: T.self))
}
return views
}
}
UPDATE 2:
Also you can have problem with incorrect height calculation if you have subview with aspect ratio constraint and at the same time proportional to superview width constraint
Since this question is a year and a half old, here is a updated and complete version, in Swift. Some of the code from the accepted answer is wrong or outdated.
func fixHeaderHeight() {
// Set your label text if needed
// ...
//
guard let header = tableView.tableHeaderView else {
return
}
let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
header.frame.height = height
tableView.tableHeaderView = header
}
Somewhere else in your code, you'll need to set the preferredMaxLayoutWidth of the label(s) in the header. This should be equal to the tableView (or screen width) minus any padding. The didSet method of your label outlet is a good place:
#IBOutlet weak var headerMessageLabel: UILabel! {
didSet {
headerMessageLabel.preferredMaxLayoutWidth = UIScreen.mainScreen().bounds.width - headerMessageLabelPadding
}
}
Note: If the accepted answer worked for you, you aren't using size classes properly.
If the header just contains a single label then I use a UILabel extension to find a multiline label height given a width:
public extension UILabel {
public class func size(withText text: String, forWidth width: CGFloat) -> CGSize {
let measurementLabel = UILabel()
measurementLabel.text = text
measurementLabel.numberOfLines = 0
measurementLabel.lineBreakMode = .byWordWrapping
measurementLabel.translatesAutoresizingMaskIntoConstraints = false
measurementLabel.widthAnchor.constraint(equalToConstant: width).isActive = true
let size = measurementLabel.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
return size
}
}
Note: the above is in Swift 3 syntax.
With the size calculated above you can return the correct height in the UITableViewDelegate method:
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
The proper solution for this problem in Swift 3 is using this class instead of standard UILabel:
class UILabelPreferedWidth : UILabel {
override var bounds: CGRect {
didSet {
if (bounds.size.width != oldValue.size.width) {
self.setNeedsUpdateConstraints()
}
}
}
override func updateConstraints() {
if(preferredMaxLayoutWidth != bounds.size.width) {
preferredMaxLayoutWidth = bounds.size.width
}
super.updateConstraints()
}
}
Also make sure that you didn't disable those two on your Cell class:
translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
I had a same problem.
This works well for me on iOS 8.4.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
[self.myLabel sizeToFit];
[self.tableView.tableHeaderView layoutIfNeeded];
return UITableViewAutomaticDimension;
}
I have a view that is laid out completely using auto layout programmatically. I have a UITextView in the middle of the view with items above and below it. Everything works fine, but I want to be able to expand UITextView as text is added. This should push everything below it down as it expands.
I know how to do this the "springs and struts" way, but is there an auto layout way of doing this? The only way I can think of is by removing and re-adding the constraint every time it needs to grow.
Summary: Disable scrolling of your text view, and don't constraint its height.
To do this programmatically, put the following code in viewDidLoad:
let textView = UITextView(frame: .zero, textContainer: nil)
textView.backgroundColor = .yellow // visual debugging
textView.isScrollEnabled = false // causes expanding height
view.addSubview(textView)
// Auto Layout
textView.translatesAutoresizingMaskIntoConstraints = false
let safeArea = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
textView.topAnchor.constraint(equalTo: safeArea.topAnchor),
textView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor)
])
To do this in Interface Builder, select the text view, uncheck Scrolling Enabled in the Attributes Inspector, and add the constraints manually.
Note: If you have other view/s above/below your text view, consider using a UIStackView to arrange them all.
Here's a solution for people who prefer to do it all by auto layout:
In Size Inspector:
Set content compression resistance priority vertical to 1000.
Lower the priority of constraint height by click "Edit" in Constraints. Just make it less than 1000.
In Attributes Inspector:
Uncheck "Scrolling Enabled"
UITextView doesn't provide an intrinsicContentSize, so you need to subclass it and provide one. To make it grow automatically, invalidate the intrinsicContentSize in layoutSubviews. If you use anything other than the default contentInset (which I do not recommend), you may need to adjust the intrinsicContentSize calculation.
#interface AutoTextView : UITextView
#end
#import "AutoTextView.h"
#implementation AutoTextView
- (void) layoutSubviews
{
[super layoutSubviews];
if (!CGSizeEqualToSize(self.bounds.size, [self intrinsicContentSize])) {
[self invalidateIntrinsicContentSize];
}
}
- (CGSize)intrinsicContentSize
{
CGSize intrinsicContentSize = self.contentSize;
// iOS 7.0+
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0f) {
intrinsicContentSize.width += (self.textContainerInset.left + self.textContainerInset.right ) / 2.0f;
intrinsicContentSize.height += (self.textContainerInset.top + self.textContainerInset.bottom) / 2.0f;
}
return intrinsicContentSize;
}
#end
The view containing UITextView will be assigned its size with setBounds by AutoLayout. So, this is what I did. The superview is initially set up all the other constraints as they should be, and in the end I put one special constraint for UITextView's height, and I saved it in an instance variable.
_descriptionHeightConstraint = [NSLayoutConstraint constraintWithItem:_descriptionTextView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:0.f
constant:100];
[self addConstraint:_descriptionHeightConstraint];
In the setBounds method, I then changed the value of the constant.
-(void) setBounds:(CGRect)bounds
{
[super setBounds:bounds];
_descriptionTextView.frame = bounds;
CGSize descriptionSize = _descriptionTextView.contentSize;
[_descriptionHeightConstraint setConstant:descriptionSize.height];
[self layoutIfNeeded];
}
You can do it through storyboard, just disable "Scrolling Enabled":)
I've found it's not entirely uncommon in situations where you may still need isScrollEnabled set to true to allow a reasonable UI interaction. A simple case for this is when you want to allow an auto expanding text view but still limit it's maximum height to something reasonable in a UITableView.
Here's a subclass of UITextView I've come up with that allows auto expansion with auto layout but that you could still constrain to a maximum height and which will manage whether the view is scrollable depending on the height. By default the view will expand indefinitely if you have your constraints setup that way.
import UIKit
class FlexibleTextView: UITextView {
// limit the height of expansion per intrinsicContentSize
var maxHeight: CGFloat = 0.0
private let placeholderTextView: UITextView = {
let tv = UITextView()
tv.translatesAutoresizingMaskIntoConstraints = false
tv.backgroundColor = .clear
tv.isScrollEnabled = false
tv.textColor = .disabledTextColor
tv.isUserInteractionEnabled = false
return tv
}()
var placeholder: String? {
get {
return placeholderTextView.text
}
set {
placeholderTextView.text = newValue
}
}
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
isScrollEnabled = false
autoresizingMask = [.flexibleWidth, .flexibleHeight]
NotificationCenter.default.addObserver(self, selector: #selector(UITextInputDelegate.textDidChange(_:)), name: Notification.Name.UITextViewTextDidChange, object: self)
placeholderTextView.font = font
addSubview(placeholderTextView)
NSLayoutConstraint.activate([
placeholderTextView.leadingAnchor.constraint(equalTo: leadingAnchor),
placeholderTextView.trailingAnchor.constraint(equalTo: trailingAnchor),
placeholderTextView.topAnchor.constraint(equalTo: topAnchor),
placeholderTextView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var text: String! {
didSet {
invalidateIntrinsicContentSize()
placeholderTextView.isHidden = !text.isEmpty
}
}
override var font: UIFont? {
didSet {
placeholderTextView.font = font
invalidateIntrinsicContentSize()
}
}
override var contentInset: UIEdgeInsets {
didSet {
placeholderTextView.contentInset = contentInset
}
}
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
if size.height == UIViewNoIntrinsicMetric {
// force layout
layoutManager.glyphRange(for: textContainer)
size.height = layoutManager.usedRect(for: textContainer).height + textContainerInset.top + textContainerInset.bottom
}
if maxHeight > 0.0 && size.height > maxHeight {
size.height = maxHeight
if !isScrollEnabled {
isScrollEnabled = true
}
} else if isScrollEnabled {
isScrollEnabled = false
}
return size
}
#objc private func textDidChange(_ note: Notification) {
// needed incase isScrollEnabled is set to true which stops automatically calling invalidateIntrinsicContentSize()
invalidateIntrinsicContentSize()
placeholderTextView.isHidden = !text.isEmpty
}
}
As a bonus there's support for including placeholder text similar to UILabel.
I see multiple answers suggest simply turning off scrollEnabled. This is the best solution. I’m writing this answer to explain why it works.
UITextView implements the intrinsicContentSize property only if scrollEnabled == NO. The disassembly of the getter method looks like this:
- (CGSize)intrinsicContentSize {
if (self.scrollEnabled) {
return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);
} else {
// Calculate and return intrinsic content size based on current width.
}
}
That means you just need to make sure the width of the text view is constrained enough and then you can make use of the intrinsic content height, either via Auto Layout content hugging/compression resistance priorities or directly using the value during manual layout.
Unfortunately, this behavior is not documented. Apple could have easily saved us all some headaches… no need for an extra height constraint, subclassing, etc.
You can also do it without subclassing UITextView. Have a look at my answer to How do I size a UITextView to its content on iOS 7?
Use the value of this expression:
[textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)].height
to update the constant of the textView's height UILayoutConstraint.
This more of a very important comment
Key to understanding why vitaminwater's answer works are three things:
Know that UITextView is a subclass of UIScrollView class
Understand how ScrollView works and how its contentSize is calculated. For more see this here answer and its various solutions and comments.
Understand what contentSize is and how its calculated. See here and here. It might also help that setting contentOffset is likely nothing but:
func setContentOffset(offset: CGPoint)
{
CGRect bounds = self.bounds
bounds.origin = offset
self.bounds = bounds
}
For more see objc scrollview and understanding scrollview
Combining the three together you'd easily understand that you need allow the the textView's intrinsic contentSize to work along AutoLayout constraints of the textView to drive the logic. It's almost as if you're textView is functioning like a UILabel
To make that happen you need to disable scrolling which basically means the scrollView's size, the contentSize's size and in case of adding a containerView, then the containerView's size would all be the same. When they're the same you have NO scrolling. And you'd have 0 contentOffset. Having 0 contentOffSet means you've not scrolled down. Not even a 1 point down! As a result the textView will be all stretched out.
It's also worth nothing that 0 contentOffset means that the scrollView's bounds and frame are identical.
If you scroll down 5 points then your contentOffset would be 5, while your scrollView.bounds.origin.y - scrollView.frame.origin.y would be equal to 5
I needed a text view that would automatically grow up until a certain maximum height, then become scrollable. Michael Link's answer worked great but I wanted to see if I could come up with something a bit simpler. Here's what I came up with:
Swift 5.3, Xcode 12
class AutoExpandingTextView: UITextView {
private var heightConstraint: NSLayoutConstraint!
var maxHeight: CGFloat = 100 {
didSet {
heightConstraint?.constant = maxHeight
}
}
private var observer: NSObjectProtocol?
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
heightConstraint = heightAnchor.constraint(equalToConstant: maxHeight)
observer = NotificationCenter.default.addObserver(forName: UITextView.textDidChangeNotification, object: nil, queue: .main) { [weak self] _ in
guard let self = self else { return }
self.heightConstraint.isActive = self.contentSize.height > self.maxHeight
self.isScrollEnabled = self.contentSize.height > self.maxHeight
self.invalidateIntrinsicContentSize()
}
}
}
An important thing to note:
Since UITextView is a subclass of UIScrollView, it is subject to the automaticallyAdjustsScrollViewInsets property of UIViewController.
If you are setting up the layout and the TextView is the the first subview in a UIViewControllers hierarchy, it will have its contentInsets modified if automaticallyAdjustsScrollViewInsets is true sometimes causing unexpected behaviour in auto layout.
So if you're having problems with auto layout and text views, try setting automaticallyAdjustsScrollViewInsets = false on the view controller or moving the textView forward in the hierarchy.
Plug and Play Solution - Xcode 9
Autolayout just like UILabel, with the link detection, text selection, editing and scrolling of UITextView.
Automatically handles
Safe area
Content insets
Line fragment padding
Text container insets
Constraints
Stack views
Attributed strings
Whatever.
A lot of these answers got me 90% there, but none were fool-proof.
Drop in this UITextView subclass and you're good.
#pragma mark - Init
- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer
{
self = [super initWithFrame:frame textContainer:textContainer];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit
{
// Try to use max width, like UILabel
[self setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
// Optional -- Enable / disable scroll & edit ability
self.editable = YES;
self.scrollEnabled = YES;
// Optional -- match padding of UILabel
self.textContainer.lineFragmentPadding = 0.0;
self.textContainerInset = UIEdgeInsetsZero;
// Optional -- for selecting text and links
self.selectable = YES;
self.dataDetectorTypes = UIDataDetectorTypeLink | UIDataDetectorTypePhoneNumber | UIDataDetectorTypeAddress;
}
#pragma mark - Layout
- (CGFloat)widthPadding
{
CGFloat extraWidth = self.textContainer.lineFragmentPadding * 2.0;
extraWidth += self.textContainerInset.left + self.textContainerInset.right;
if (#available(iOS 11.0, *)) {
extraWidth += self.adjustedContentInset.left + self.adjustedContentInset.right;
} else {
extraWidth += self.contentInset.left + self.contentInset.right;
}
return extraWidth;
}
- (CGFloat)heightPadding
{
CGFloat extraHeight = self.textContainerInset.top + self.textContainerInset.bottom;
if (#available(iOS 11.0, *)) {
extraHeight += self.adjustedContentInset.top + self.adjustedContentInset.bottom;
} else {
extraHeight += self.contentInset.top + self.contentInset.bottom;
}
return extraHeight;
}
- (void)layoutSubviews
{
[super layoutSubviews];
// Prevents flashing of frame change
if (CGSizeEqualToSize(self.bounds.size, self.intrinsicContentSize) == NO) {
[self invalidateIntrinsicContentSize];
}
// Fix offset error from insets & safe area
CGFloat textWidth = self.bounds.size.width - [self widthPadding];
CGFloat textHeight = self.bounds.size.height - [self heightPadding];
if (self.contentSize.width <= textWidth && self.contentSize.height <= textHeight) {
CGPoint offset = CGPointMake(-self.contentInset.left, -self.contentInset.top);
if (#available(iOS 11.0, *)) {
offset = CGPointMake(-self.adjustedContentInset.left, -self.adjustedContentInset.top);
}
if (CGPointEqualToPoint(self.contentOffset, offset) == NO) {
self.contentOffset = offset;
}
}
}
- (CGSize)intrinsicContentSize
{
if (self.attributedText.length == 0) {
return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);
}
CGRect rect = [self.attributedText boundingRectWithSize:CGSizeMake(self.bounds.size.width - [self widthPadding], CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
return CGSizeMake(ceil(rect.size.width + [self widthPadding]),
ceil(rect.size.height + [self heightPadding]));
}
vitaminwater's answer is working for me.
If your textview's text is bouncing up and down during edit, after setting [textView setScrollEnabled:NO];, set Size Inspector > Scroll View > Content Insets > Never.
Hope it helps.
Place hidden UILabel underneath your textview. Label lines = 0. Set constraints of UITextView to be equal to the UILabel (centerX, centerY, width, height). Works even if you leave scroll behaviour of textView.
BTW, I built an expanding UITextView using a subclass and overriding intrinsic content size. I discovered a bug in UITextView that you might want to investigate in your own implementation. Here is the problem:
The expanding text view would grow down to accommodate the growing text if you type single letters at a time. But if you paste a bunch of text into it, it would not grow down but the text would scroll up and the text at the top was out of view.
The solution:
Override setBounds: in your subclass. For some unknown reason, the pasting caused the bounds.origin.y value to be non-zee (33 in every case that I saw). So I overrode setBounds: to always set the bounds.origin.y to zero. Fixed the problem.
Here's a quick solution:
This problem may occur if you have set clipsToBounds property to false of your textview. If you simply delete it, the problem goes away.
myTextView.clipsToBounds = false //delete this line
Obj C:
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#property (nonatomic) UITextView *textView;
#end
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize textView;
- (void)viewDidLoad{
[super viewDidLoad];
[self.view setBackgroundColor:[UIColor grayColor]];
self.textView = [[UITextView alloc] initWithFrame:CGRectMake(30,10,250,20)];
self.textView.delegate = self;
[self.view addSubview:self.textView];
}
- (void)didReceiveMemoryWarning{
[super didReceiveMemoryWarning];
}
- (void)textViewDidChange:(UITextView *)txtView{
float height = txtView.contentSize.height;
[UITextView beginAnimations:nil context:nil];
[UITextView setAnimationDuration:0.5];
CGRect frame = txtView.frame;
frame.size.height = height + 10.0; //Give it some padding
txtView.frame = frame;
[UITextView commitAnimations];
}
#end