TIMEX
TIMEX

Reputation: 272094

How do I create UITableView header whose height is determined by the height of its label?

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

Upvotes: 13

Views: 1596

Answers (6)

Elangovan
Elangovan

Reputation: 1206

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

Upvotes: -2

Satyanarayana
Satyanarayana

Reputation: 1067

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

Upvotes: -1

suite22
suite22

Reputation: 476

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.

Upvotes: 2

Beslan Tularov
Beslan Tularov

Reputation: 3131

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

Upvotes: 10

Sulthan
Sulthan

Reputation: 130132

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

Upvotes: 9

stefandouganhyde
stefandouganhyde

Reputation: 4554

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.

Upvotes: 20

Related Questions