Home > OS >  How do you install a tableHeaderView on a UITableView?
How do you install a tableHeaderView on a UITableView?

Time:09-17

After a decade, I suspected no one has actually asked this question directly. There are many questions asking how to fix a tableHeaderView layout problem caused by rotation for example. But the real question is, how did Apple intend for this to work?

Auto-layout does not seem to play ball with tableHeaderView, as you can see in this almost 9 year old post

enter image description here

final class EventDetailTableHeaderView: UIView {
    
    private let titleContainer: TitleContainerView
    private let subtitleContainer: SubtitleContainerView
    
    init(_ width: CGFloat, event: CloudEvent) {
        
        let size = CGSize(width: width, height: 0)
        let frame = CGRect(origin: .zero, size: size)
        
        titleContainer = TitleContainerView(frame: frame, text: event.title)
        subtitleContainer = SubtitleContainerView(frame: frame, text: event.displayString)
        
        super.init(frame: frame)
        
        backgroundColor = StyleKit.wDOWhite
        autoresizingMask = [.flexibleWidth]
        
        setupSubviews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupSubviews() {
        setupTitleContiner()
        setupSubtitleContainer()
    }
    
    private func setupTitleContiner() {
        addSubview(titleContainer)
        titleContainer.autoresizingMask = [.flexibleWidth]
        titleContainer.backgroundColor = StyleKit.wDOWhite
    }
    
    private func setupSubtitleContainer() {
        addSubview(subtitleContainer)
        subtitleContainer.autoresizingMask = [.flexibleWidth]
        subtitleContainer.backgroundColor = StyleKit.wDOBlue
    }
        
    override func layoutSubviews() {
        super.layoutSubviews()
        positionSubtitleContainer()
        frame = CGRect(
            origin: .zero,
            size: calculateSize()
        )
    }
    
    
    private func positionSubtitleContainer() {
        subtitleContainer.frame.origin.y = titleContainer.frame.height
    }
        
    private func calculateSize() -> CGSize {
        CGSize(
            width: frame.width,
            height: calculateHeightOfSubviews()
        )
    }
    
    private func calculateHeightOfSubviews() -> CGFloat {
        let titleContainerHeight = titleContainer.frame.height
        let subtitleContainerHeight = subtitleContainer.frame.height
        return titleContainerHeight   subtitleContainerHeight
    }
}

final class TitleContainerView: UIView {
    
    private static let font = FontManagement.fontWithStyle(.heavy, withSize: 32.0)
    
    private let label: UILabel = {
        let label = UILabel()
        label.autoresizingMask = [.flexibleWidth]
        label.numberOfLines = 0
        label.backgroundColor = StyleKit.wDOWhite
        label.font = TitleContainerView.font
        label.textColor = StyleKit.wDOBlue
        return label
    }()
    
    convenience init(frame: CGRect, text: String) {
        let font = TitleContainerView.font
        let labelFrame = TitleContainerView.establishLabelFrame(frame, text, font)
        var frame = frame
        frame.size.height = TitleContainerView.establishHeight(labelFrame)
        self.init(frame: frame)
        label.text = text
        label.frame = labelFrame
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(label)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private static let insets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
    
    override func layoutSubviews() {
        super.layoutSubviews()
        let font = label.font!
        let text = label.text ?? ""
        label.frame = Self.establishLabelFrame(frame, text, font)
        frame.size.height = Self.establishHeight(label.frame)
    }
        
    private static func establishLabelFrame(_ frame: CGRect, _ text: String, _ font: UIFont) -> CGRect {
        let size = establishLabelSize(frame, text, font)
        let origin = establishLabelOrigin(frame, size)
        return CGRect(origin: origin, size: size)
    }
    
    private static func establishLabelSize(_ frame: CGRect, _ text: String, _ font: UIFont) -> CGSize {
        let width = frame.width - TitleContainerView.insets.left - TitleContainerView.insets.right
        let height = text.height(withConstrainedWidth: width, font: font)
        return CGSize(
            width: width,
            height: height
        )
    }
    
    private static func establishLabelOrigin(_ frame: CGRect, _ size: CGSize) -> CGPoint {
        CGPoint(
            x: (frame.width - size.width) / 2.0,
            y: (frame.height - size.height) / 2.0
        )
    }
    
    private static func establishHeight(_ labelFrame: CGRect) -> CGFloat {
        labelFrame.size.height   TitleContainerView.insets.top   TitleContainerView.insets.bottom
    }
}

extension String {

    func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
        return ceil(boundingBox.height)
    }
}

override func viewDidLoad() {
        super.viewDidLoad()
                        
        tableView = EventDetailTableView(frame: .zero, style: .plain)
        tableView?.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView!) 
        
        let width = view.bounds.width
        let tableHeaderView = EventDetailTableHeaderView(width, event: event)
        tableHeaderView.layoutIfNeeded()
        tableView?.tableHeaderView = tableHeaderView
                
        NSLayoutConstraint.activate([
            view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: tableView!.topAnchor),
            view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: tableView!.trailingAnchor),
            view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: tableView!.leadingAnchor),
            view.bottomAnchor.constraint(equalTo: tableView!.bottomAnchor)
        ])
}

enter image description here

CodePudding user response:

While I agree it seems like there would be a more straight-forward way of implementing an auto-height-sizing tableHeaderView, a common approach is to use auto-layout and an extension like this:

extension UITableView {
    func sizeHeaderToFit() {
        guard let headerView = tableHeaderView else { return }
        
        let newHeight = headerView.systemLayoutSizeFitting(CGSize(width: frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
        var frame = headerView.frame
        
        // avoids infinite loop!
        if newHeight.height != frame.height {
            frame.size.height = newHeight.height
            headerView.frame = frame
            tableHeaderView = headerView
        }
    }
}

We call that from within viewDidLayoutSubviews():

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    tableView.sizeHeaderToFit()
}

Here is a complete example, which should come pretty close to your layout:

class TestViewController: UIViewController {
    
    let tableView = UITableView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(tableView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: g.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.dataSource = self
        tableView.delegate = self
        
        let hView = EventDetailTableHeaderView(titleText: "Street Dance Championships", subTitleText: "4 June 2019  |  8:30 AM to 5:30 PM  |  Sports Wales National Centre  |  Cardiff")
        tableView.tableHeaderView = hView
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.sizeHeaderToFit()
    }
}

extension TestViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 30
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        c.textLabel?.text = "\(indexPath)"
        return c
    }
}

extension UITableView {
    func sizeHeaderToFit() {
        guard let headerView = tableHeaderView else { return }
        
        let newHeight = headerView.systemLayoutSizeFitting(CGSize(width: frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
        var frame = headerView.frame
        
        // avoids infinite loop!
        if newHeight.height != frame.height {
            frame.size.height = newHeight.height
            headerView.frame = frame
            tableHeaderView = headerView
        }
    }
}

class TitleContainerView: UIView {

    private static let font: UIFont = .systemFont(ofSize: 32, weight: .heavy)
    
    let label: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        v.textColor = UIColor(red: 0.044, green: 0.371, blue: 0.655, alpha: 1.0)
        v.font = TitleContainerView.font
        return v
    }()

    convenience init(text: String) {
        self.init(frame: .zero)
        label.text = text
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        backgroundColor = UIColor(red: 0.93, green: 0.94, blue: 0.95, alpha: 1.0)
        label.translatesAutoresizingMaskIntoConstraints = false
        addSubview(label)
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
            label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
            label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
            label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
        ])
    }

}
class SubtitleContainerView: UIView {
    
    private static let font: UIFont = .systemFont(ofSize: 20, weight: .bold)
    
    let label: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        v.textColor = .white
        v.font = SubtitleContainerView.font
        return v
    }()
    
    convenience init(text: String) {
        self.init(frame: .zero)
        label.text = text
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        backgroundColor = UIColor(red: 0.044, green: 0.371, blue: 0.655, alpha: 1.0)
        label.translatesAutoresizingMaskIntoConstraints = false
        addSubview(label)
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
            label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
            label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
            label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
        ])
    }
}

class EventDetailTableHeaderView: UIView {
    
    var titleView: TitleContainerView!
    var subTitleView: SubtitleContainerView!
    
    convenience init(titleText: String, subTitleText: String) {
        self.init(frame: .zero)
        titleView = TitleContainerView(text: titleText)
        subTitleView = SubtitleContainerView(text: subTitleText)
        commonInit()
    }
    
    func commonInit() -> Void {
        titleView.translatesAutoresizingMaskIntoConstraints = false
        subTitleView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(titleView)
        addSubview(subTitleView)
        
        // this avoids auto-layout complaints
        let titleViewTrailingConstraint = titleView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0)
        titleViewTrailingConstraint.priority = UILayoutPriority(rawValue: 999)
        let subTitleViewBottomConstraint = subTitleView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
        subTitleViewBottomConstraint.priority = UILayoutPriority(rawValue: 999)
        
        NSLayoutConstraint.activate([
            titleView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
            titleView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
            
            titleViewTrailingConstraint,
            
            subTitleView.topAnchor.constraint(equalTo: titleView.bottomAnchor, constant: 0.0),
            subTitleView.leadingAnchor.constraint(equalTo: titleView.leadingAnchor, constant: 0.0),
            subTitleView.trailingAnchor.constraint(equalTo: titleView.trailingAnchor, constant: 0.0),
            
            subTitleViewBottomConstraint,
        ])
        
    }

}

and the output looks like this:

enter image description here

enter image description here


Edit -- same output, but using auto-layout only for adding the tableView to the main view.

Class names prefixed with RM_ (for Resizing Mask):

class RM_TestViewController: UIViewController {
    
    let tableView = UITableView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(tableView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: g.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.dataSource = self
        tableView.delegate = self
        
        let hView = RM_EventDetailTableHeaderView(titleText: "Street Dance Championships", subTitleText: "4 June 2019  |  8:30 AM to 5:30 PM  |  Sports Wales National Centre  |  Cardiff")
        tableView.tableHeaderView = hView
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.rm_sizeHeaderToFit()
    }
}

extension RM_TestViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 30
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        c.textLabel?.text = "\(indexPath)"
        return c
    }
}

extension UITableView {
    func rm_sizeHeaderToFit() {
        guard let headerView = tableHeaderView as? RM_EventDetailTableHeaderView else { return }
        
        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()
        
        // avoids infinite loop!
        if headerView.myHeight != headerView.frame.height {
            headerView.frame.size.height = headerView.myHeight
            tableHeaderView = headerView
        }
    }
}


class RM_TitleContainerView: UIView {
    
    private static let font: UIFont = .systemFont(ofSize: 32, weight: .heavy)
    
    let label: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        v.textColor = UIColor(red: 0.044, green: 0.371, blue: 0.655, alpha: 1.0)
        v.font = RM_TitleContainerView.font
        // during dev, so we can see the label frame
        //v.backgroundColor = .green
        return v
    }()
    
    convenience init(text: String) {
        self.init(frame: .zero)
        label.text = text
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        backgroundColor = UIColor(red: 0.93, green: 0.94, blue: 0.95, alpha: 1.0)
        addSubview(label)
        label.frame.origin = CGPoint(x: 8, y: 8)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        label.frame.size.width = bounds.width - 16
        let sz = label.systemLayoutSizeFitting(CGSize(width: label.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
        label.frame.size.height = sz.height
    }

    var myHeight: CGFloat {
        get {
            return label.frame.height   16.0
        }
    }
}
class RM_SubtitleContainerView: UIView {
    
    private static let font: UIFont = .systemFont(ofSize: 20, weight: .bold)
    
    let label: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        v.textColor = .white
        v.font = RM_SubtitleContainerView.font
        // during dev, so we can see the label frame
        //v.backgroundColor = .systemYellow
        return v
    }()
    
    convenience init(text: String) {
        self.init(frame: .zero)
        label.text = text
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        backgroundColor = UIColor(red: 0.044, green: 0.371, blue: 0.655, alpha: 1.0)
        addSubview(label)
        label.frame.origin = CGPoint(x: 8, y: 8)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        label.frame.size.width = bounds.width - 16
        let sz = label.systemLayoutSizeFitting(CGSize(width: label.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
        label.frame.size.height = sz.height
    }
    
    var myHeight: CGFloat {
        get {
            return label.frame.height   16.0
        }
    }
}

class RM_EventDetailTableHeaderView: UIView {
    
    var titleView: RM_TitleContainerView!
    var subTitleView: RM_SubtitleContainerView!
    
    convenience init(titleText: String, subTitleText: String) {
        self.init(frame: .zero)
        titleView = RM_TitleContainerView(text: titleText)
        subTitleView = RM_SubtitleContainerView(text: subTitleText)
        commonInit()
    }
    
    func commonInit() -> Void {
        addSubview(titleView)
        addSubview(subTitleView)
        
        // initial height doesn't matter
        titleView.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: 8)
        subTitleView.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: 8)
        
        titleView.autoresizingMask = [.flexibleWidth]
        subTitleView.autoresizingMask = [.flexibleWidth]
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // force subviews to update
        titleView.setNeedsLayout()
        subTitleView.setNeedsLayout()
        titleView.layoutIfNeeded()
        subTitleView.layoutIfNeeded()
        
        // get subview heights
        titleView.frame.size.height = titleView.myHeight
        subTitleView.frame.origin.y = titleView.frame.maxY
        subTitleView.frame.size.height = subTitleView.myHeight
    }
    
    var myHeight: CGFloat {
        get {
            return subTitleView.frame.maxY
        }
    }
}
  • Related