'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:

enter image description here

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

enter image description here

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