13

I would like to add a header to my tableView. This header contains 1 UILabel. The header height should be calculated based on the number of lines the label has.

In my code, I'm adding constraints with all the edges of the label <> header. This is my attempt:

    //Add header to tableView
    header = UIView()
    header.backgroundColor = UIColor.yellowColor()
    tableView!.tableHeaderView = header

    //Create Label and add it to the header
    postBody = UILabel()
    postBody.text = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."
    postBody.font = UIFont(name: "Lato-Regular", size: 16.0)
    postBody.numberOfLines = 0
    postBody.backgroundColor = FlatLime()
    header.addSubview(postBody)

    //Enable constraints for each item
    postBody.translatesAutoresizingMaskIntoConstraints = false
    header.translatesAutoresizingMaskIntoConstraints = false

    //Add constraints to the header and post body
    let postBodyLeadingConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Leading, multiplier: 1, constant: 0)
    postBodyLeadingConstraint.active = true

    let postBodyTrailingConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Trailing, multiplier: 1, constant: 0)
    postBodyTrailingConstraint.active = true


    let postBodyTopConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 0)
    postBodyTopConstraint.active = true


    let postBodyBottomConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0)
    postBodyBottomConstraint.active = true


    //Calculate header size
    let size = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
    var frame = header.frame
    frame.size.height = size.height
    header.frame = frame
    tableView!.tableHeaderView = header
    header.layoutIfNeeded()

This is my table:

    let nib = UINib(nibName: "MessagesTableViewCell", bundle: nil)
    let nibSimple = UINib(nibName: "SimpleMessagesTableViewCell", bundle: nil)
    self.tableView!.registerNib(nib, forCellReuseIdentifier: "MessagesTableViewCell")
    self.tableView!.registerNib(nibSimple, forCellReuseIdentifier: "SimpleMessagesTableViewCell")
    self.tableView!.dataSource = self
    self.tableView!.delegate = self
    self.tableView!.rowHeight = UITableViewAutomaticDimension
    self.tableView!.estimatedRowHeight = 100.0
    self.tableView!.separatorStyle = UITableViewCellSeparatorStyle.None
    self.tableView!.separatorColor = UIColor(hex: 0xf5f5f5)
    self.tableView!.separatorInset = UIEdgeInsetsMake(0, 0, 0, 0)
    self.tableView!.clipsToBounds = true
    self.tableView!.allowsSelection = false
    self.tableView!.allowsMultipleSelection = false
    self.tableView!.keyboardDismissMode = .OnDrag

As you can see, the header does not take into account the height of the label (which I did numberOfLines = 0)

Header does not auto-height

TIMEX
  • 259,804
  • 351
  • 777
  • 1,080
  • Can you post your table view primary method code? – Viral Savaj Apr 26 '16 at 06:53
  • If you set a breakpoint what does the frame look like when you assign it back to the header? – Joe Apr 26 '16 at 06:57
  • Are you not missing postBody.translatesAutoresizingMaskIntoConstraints = false ? – Terry Apr 26 '16 at 07:56
  • Does it need to be a header, or can you constrain a header view on top of the tableView? – Doug Mead Apr 28 '16 at 20:01
  • Also, it doesn't look like you're resizing the label to a certain width. This could drive the height of it. Not sure if you have it somewhere else in the code? – Doug Mead Apr 28 '16 at 20:02
  • @DougMead I have constraints on both sides of the label to its superview. Why do I need to set a constraint for width? Also, what do you mea "constrain a header on top of it"? How would that work/look like? – TIMEX Apr 29 '16 at 00:55
  • Get relevant code idea from this blog post:- http://www.programmingcrew.in/2015/09/uitable-view-cell-dynamic-height-ios7.html – pkc456 Apr 29 '16 at 08:29
  • There's a great blog post about this over here http://roadfiresoftware.com/2015/05/how-to-size-a-table-header-view-using-auto-layout-in-interface-builder/. This solves exactly the issue you are facing. Hope this helps. – riik Apr 29 '16 at 13:42
  • did you set word wrapping on the label? You are setting `header.translatesAutoresizingMaskIntoConstraints = false` and then you are changing `frame` of the `header`? That's very strange. – Sulthan Apr 29 '16 at 15:17

6 Answers6

18

UILabels take advantage of UIView's intrinsicContentSize() to tell auto layout what size they should be. For a multiline label, however, the intrinsic content size is ambiguous; the table doesn't know if it should be short and wide, tall and narrow, or anything in between.

To combat this, UILabel has a property called preferredMaxLayoutWidth. Setting this tells a multiline label that it should be at most this wide, and allows intrinsicContentSize() to figure out and return an appropriate height to match. By not setting the preferredMaxLayoutWidth in your example, the label leaves its width unbounded and therefore calculates the height for a long, single line of text.

The only complication with preferredMaxLayoutWidth is that you typically don't know what width you want the label to be until auto layout has calculated one for you. For that reason, the place to set it in a view controller subclass (which it looks like your code sample might be from) is in viewDidLayoutSubviews:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    postBody.preferredMaxLayoutWidth = CGRectGetWidth(postBody.frame)
    // then update the table header view
    if let header = tableView?.tableHeaderView {
        header.frame.size.height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
        tableView?.tableHeaderView = header
    }
}

Obviously, you'll need to add a property for the postBody label for this to work.

Let me know if you're not in a UIViewController subclass here and I'll edit my answer.

stefandouganhyde
  • 4,494
  • 1
  • 16
  • 13
10

Implementation using the storyboard

  1. In UItableView add on UITableViewCell new UIView and put him UILabel Connects them via Autolayout
  2. In UILabel put the number of lines to 0.
  3. In ViewDidLoad your UILabel call a method sizeToFit() and specify a size for UIView, and that will be your HeaderVew headerView.frame.size.height = headerLabel.frame.size.height

Code

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var headerView: UIView!
    @IBOutlet weak var headerLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        headerLabel.text = "tableViewdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarningdidReceiveMemoryWarning"
        headerLabel.sizeToFit()
        headerView.frame.size.height = headerLabel.frame.size.height
    }

ScreenShot

enter image description here

TestProject

test project link

Beslan Tularov
  • 3,111
  • 1
  • 21
  • 34
9

The first problem we have is that the header cannot be resized by autolayout, for details, see Is it possible to use AutoLayout with UITableView's tableHeaderView?

Therefore, we have to calculate the height of the header manually, for example:

@IBOutlet var table: UITableView!

var header: UIView?
var postBody: UILabel?

override func viewDidLoad() {
    super.viewDidLoad()

    let header = UIView()
    // don't forget to set this
    header.translatesAutoresizingMaskIntoConstraints = true
    header.backgroundColor = UIColor.yellowColor()

    let postBody = UILabel()
    postBody.translatesAutoresizingMaskIntoConstraints = false
    postBody.text = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."
    postBody.font = UIFont.systemFontOfSize(16.0)

    // don't forget to set this
    postBody.lineBreakMode = .ByWordWrapping
    postBody.numberOfLines = 0

    header.addSubview(postBody)

    let leadingConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Leading, multiplier: 1, constant: 0)
    let trailingConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Trailing, multiplier: 1, constant: 0)
    let topConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 0)
    let bottomConstraint = NSLayoutConstraint(item: postBody, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: header, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0)

    header.addConstraints([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])

    self.table.tableHeaderView = header

    self.header = header
    self.postBody = postBody
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    let text = postBody!.attributedText!

    let height = text.boundingRectWithSize(
        CGSizeMake(table.bounds.size.width, CGFloat.max),
        options: [.UsesLineFragmentOrigin],
        context: nil
    ).height

    header!.frame.size.height = height
}

You might also want to use the code in stefandouganhyde's answer. It does not really matter how you calculate the height. The point is that autolayout won't work automatically for tableHeaderView.

Result:

resulting table

Community
  • 1
  • 1
Sulthan
  • 128,090
  • 22
  • 218
  • 270
2

We use NSLayoutManager to quickly estimate the height for items that need to resize based on the text. This is the basic idea:

override class func estimatedHeightForItem(text: String, atWidth width: CGFloat) -> CGFloat {

    let storage = NSTextStorage(string: text!)
    let container = NSTextContainer(size: CGSize(width: width, height: CGFloat.max))
    let layoutManager = NSLayoutManager()
    layoutManager.addTextContainer(container)
    storage.addLayoutManager(layoutManager)
    storage.addAttribute(NSFontAttributeName, value: UIFont.Body, range: NSRange(location: 0, length: storage.length))
    container.lineFragmentPadding = 0.0

    return layoutManager.usedRectForTextContainer(container).size.height 
}

Beslan's answer is probably a better fit for your use case, but I find it nice to have more control how the layout is handled.

Community
  • 1
  • 1
suite22
  • 476
  • 3
  • 16
-1

//may be it will help for you.

header = UIView(frame: CGRectMake(tableview.frame.origin.x,tableview.frame.origin.y, tableview.frame.size.width, 40))
header.backgroundColor = UIColor.yellowColor()

//Create Label and add it to the header
postBody = UILabel(frame: header.frame)
postBody.text = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."
postBody.font = UIFont(name: "Lato-Regular", size: 16.0)
postBody.numberOfLines = 0
postBody.backgroundColor = FlatLime()
header.addSubview(postBody)

let maximumLabelSize: CGSize = CGSizeMake(postBody.size.width, CGFloat.max);

let options: NSStringDrawingOptions  = NSStringDrawingOptions.UsesLineFragmentOrigin
let context: NSStringDrawingContext = NSStringDrawingContext()
        context.minimumScaleFactor = 0.8
        let attr: Dictionary = [NSFontAttributeName: postBody.font!]
        var size: CGSize? = postBody.text?.boundingRectWithSize(maximumLabelSize, options:options, attributes: attr, context: context).size

let frame = header.frame
frame.size.height = size?.height
header.frame = frame
postBody.frame = frame
tableView!.tableHeaderView = header
Satyanarayana
  • 1,059
  • 6
  • 16
-2

you can calculate the height of a label by using its string

let labelWidth = label.frame.width
let maxLabelSize = CGSize(width: labelWidth, height: CGFloat.max)
let actualLabelSize = label.text!.boundingRectWithSize(maxLabelSize, options: [.UsesLineFragmentOrigin], attributes: [NSFontAttributeName: label.font], context: nil)
let labelHeight = actualLabelSize.height
Elangovan
  • 1,158
  • 1
  • 9
  • 26