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
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)
])
}
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:
Edit -- same output, but using auto-layout only for adding the tableView to the main view.
Class names prefixed with RM_
(for R
esizing 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
}
}
}