'Setting tableHeaderView height dynamically
My application creates a UITableViewController that contains a custom tableHeaderView which may have an arbitrary height. I've been struggling with a way to set this header dynamically, as it seems the suggested ways have been cutting this header short. My UITableViewController's relevant code:
import UIKit
import SafariServices
class RedditPostViewController: UITableViewController, NetworkCommunication, SubViewLaunchLinkManager {
//MARK: UITableViewDataSource
var post: PostData?
var tree: CommentTree?
weak var session: Session! = Session.sharedInstance
override func viewDidLoad() {
super.viewDidLoad()
// Get post info from api
guard let postData = post else { return }
//Configure comment table
self.tableView.registerClass(RedditPostCommentTableViewCell.self, forCellReuseIdentifier: "CommentCell")
let tableHeader = PostView(withPost: postData, inViewController: self)
let size = tableHeader.systemLayoutSizeFittingSize(UILayoutFittingExpandedSize)
let height = size.height
let width = size.width
tableHeader.frame = CGRectMake(0, 0, width, height)
self.tableView.tableHeaderView = tableHeader
session.getRedditPost(postData) { (post) in
self.post = post?.post
self.tree = post?.comments
self.tableView.reloadData()
}
}
}
This results in the following incorrect layout:
If I change the line: tableHeader.frame = CGRectMake(0, 0, width, height)
to tableHeader.frame = CGRectMake(0, 0, width, 1000)
the tableHeaderView will lay itself out correctly:
I'm not sure what I'm doing incorrectly here. Also, custom UIView class, if this helps:
import UIKit
import Foundation
protocol SubViewLaunchLinkManager: class {
func launchLink(sender: UIButton)
}
class PostView: UIView {
var body: UILabel?
var post: PostData?
var domain: UILabel?
var author: UILabel?
var selfText: UILabel?
var numComments: UILabel?
required init?(coder aDecoder: NSCoder) {
fatalError("Not implemented yet")
}
init(withPost post: PostData, inViewController viewController: SubViewLaunchLinkManager) {
super.init(frame: CGRectZero)
self.post = post
self.backgroundColor = UIColor.lightGrayColor()
let launchLink = UIButton()
launchLink.setImage(UIImage(named: "circle-user-7"), forState: .Normal)
launchLink.addTarget(viewController, action: "launchLink:", forControlEvents: .TouchUpInside)
self.addSubview(launchLink)
selfText = UILabel()
selfText?.backgroundColor = UIColor.whiteColor()
selfText?.numberOfLines = 0
selfText?.lineBreakMode = .ByWordWrapping
selfText!.text = post.selfText
self.addSubview(selfText!)
selfText?.sizeToFit()
//let attributedString = NSAttributedString(string: "Test"/*post.selfTextHtml*/, attributes: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType])
//selfText.attributedText = attributedString
body = UILabel()
body!.text = post.title
body!.numberOfLines = 0
body!.lineBreakMode = .ByWordWrapping
body!.textAlignment = .Justified
self.addSubview(body!)
domain = UILabel()
domain!.text = post.domain
self.addSubview(domain!)
author = UILabel()
author!.text = post.author
self.addSubview(author!)
numComments = UILabel()
numComments!.text = "\(post.numComments)"
self.addSubview(numComments!)
body!.translatesAutoresizingMaskIntoConstraints = false
domain!.translatesAutoresizingMaskIntoConstraints = false
author!.translatesAutoresizingMaskIntoConstraints = false
selfText!.translatesAutoresizingMaskIntoConstraints = false
launchLink.translatesAutoresizingMaskIntoConstraints = false
numComments!.translatesAutoresizingMaskIntoConstraints = false
let views: [String: UIView] = ["body": body!, "domain": domain!, "author": author!, "numComments": numComments!, "launchLink": launchLink, "selfText": selfText!]
//let selfTextSize = selfText?.sizeThatFits((selfText?.frame.size)!)
//print(selfTextSize)
//let metrics = ["selfTextHeight": selfTextSize!.height]
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[body]-[selfText]-[domain]-|", options: [], metrics: nil, views: views))
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[body]-[selfText]-[author]-|", options: [], metrics: nil, views: views))
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[body]-[selfText]-[numComments]-|", options: [], metrics: nil, views: views))
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[launchLink]-[numComments]-|", options: [], metrics: nil, views: views))
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[body][launchLink]|", options: [], metrics: nil, views: views))
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[selfText][launchLink]|", options: [], metrics: nil, views: views))
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[domain][author][numComments][launchLink]|", options: [], metrics: nil, views: views))
}
override func layoutSubviews() {
super.layoutSubviews()
body?.preferredMaxLayoutWidth = body!.bounds.width
}
}
Solution 1:[1]
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
}
}
}
Solution 2:[2]
Determining the header's frame size using
header.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
as suggested in the answers above didn't work for me when my header view consisted of a single multiline label. With the label's line break mode set to wrap, the text just gets cut off:
Instead, what did work for me was using the width of the table view and a height of 0 as the target size:
header.systemLayoutSizeFitting(CGSize(width: tableView.bounds.width, height: 0))
Putting it all together (I prefer to use an extension):
extension UITableView {
func updateHeaderViewHeight() {
if let header = self.tableHeaderView {
let newSize = header.systemLayoutSizeFitting(CGSize(width: self.bounds.width, height: 0))
header.frame.size.height = newSize.height
}
}
}
And call it like so:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
tableView.updateHeaderViewHeight()
}
Solution 3:[3]
More condensed version of OP's answer, with the benefit of allowing layout to happen naturally (note this solution uses viewWillLayoutSubviews):
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let header = tableView.tableHeaderView {
let newSize = header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
header.frame.size.height = newSize.height
}
}
Thanks to TravMatth for the original answer.
Solution 4:[4]
If you're still having problems with layout with the above code sample, there's a slight chance you disabled translatesAutoresizingMaskIntoConstraints
on the custom header view. In that case, you need to set translatesAutoresizingMaskIntoConstraints
back to true
after you set the header's frame.
Here's the code sample I'm using, and working correctly on iOS 11.
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let headerView = tableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
var headerFrame = headerView.frame
if height != headerFrame.size.height {
headerFrame.size.height = height
headerView.frame = headerFrame
tableView.tableHeaderView = headerView
if #available(iOS 9.0, *) {
tableView.layoutIfNeeded()
}
}
headerView.translatesAutoresizingMaskIntoConstraints = true
}
Solution 5:[5]
Based on @TravMatth and @NSExceptional's answer:
For Dynamic TableView Header, with multiple line of text(No matter have or not)
My solution is:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let footView = tableView.tableFooterView {
let newSize = footView.systemLayoutSizeFitting(CGSize(width: self.view.bounds.width, height: 0))
if newSize.height != footView.frame.size.height {
footView.frame.size.height = newSize.height
tableView.tableFooterView = footView
}
}
}
tableView.tableFooterView = footView to make sure that your tableview Header or Footer updated. And
if newSize.height != footView.frame.size.height
helps you not to be called this method many times
Solution 6:[6]
I use the accepted answer for a long time and it always worked for me, until today, when I used a multiple lines label in a complex table header view, I ran into the same issue @frank61003 had:
it create a blank area with multiple lines label.
So in my case, there were big vertical margins around my label. If label text is just 1 line, then everything is fine. This issue only happens when the label has multiple lines of text.
I don't know the exact reason causing this, but I dug for a while and found a workaround to solve the issue, so I want to leave a reference here in case anyone runs into the same problem.
Optional first step, make sure your multiple lines label has the lowest Content Hugging Priority in your table header view, so it can auto increase to fit its text.
Then, add this calculate label height method to your view controller
private func calculateHeightForString(_ string: String) -> CGFloat {
let yourLabelWidth = UIScreen.main.bounds.width - 20
let constraintRect = CGSize(width: yourLabelWidth, height: CGFloat.greatestFiniteMagnitude)
let rect = string.boundingRect(with: constraintRect,
options: .usesLineFragmentOrigin,
// use your label's font
attributes: [.font: descriptionLabel.font!],
context: nil)
return rect.height + 6 // give a little extra arbitrary space (6), remove it if you don't need
}
And use the method above to configure your multiple lines label in viewDidLoad
let description = "Long long long ... text"
descriptionLabel.text = description
// manually calculate multiple lines label height and add a constraint to avoid extra space bug
descriptionLabel.heightAnchor.constraint(equalToConstant: calculateHeightForString(description)).isActive = true
This solved my issue, hope it can work for you too.
Solution 7:[7]
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let headerView = self.tableView.tableHeaderView {
let headerViewFrame = headerView.frame
let height = headerView.systemLayoutSizeFitting(headerViewFrame.size, withHorizontalFittingPriority: UILayoutPriority.defaultHigh, verticalFittingPriority: UILayoutPriority.defaultLow).height
var headerFrame = headerView.frame
if height != headerFrame.size.height {
headerFrame.size.height = height
headerView.frame = headerFrame
self.tableView.tableHeaderView = headerView
}
}
}
Problem in calculating label size when using horizontal or vertical fitting
Solution 8:[8]
If all constraint is added, this will work:
headerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
Solution 9:[9]
**My Working Solution is:
Add this function in viewcontroller**
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let headerView = myTableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
var headerFrame = headerView.frame
if height != headerFrame.size.height {
headerFrame.size.height = height
headerView.frame = headerFrame
myTableView.tableHeaderView = headerView
if #available(iOS 9.0, *) {
myTableView.layoutIfNeeded()
}
}
headerView.translatesAutoresizingMaskIntoConstraints = true
}
**Add one line in header's view class.**
override func layoutSubviews() {
super.layoutSubviews()
bookingLabel.preferredMaxLayoutWidth = bookingLabel.bounds.width
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Sunil Targe |
Solution 2 | NSExceptional |
Solution 3 | |
Solution 4 | |
Solution 5 | |
Solution 6 | Daniel Hu |
Solution 7 | Khoren Asatryan |
Solution 8 | hstdt |
Solution 9 | Gurpreet Singh |