2

I have a "plain" style UITableView. I am setting a view as the tableViewHeader for the table view. The table also shows the section index down the right side.

My issue is figuring out how to inset the left and right edge of the header view to take into account safe area insets if run on an iPhone X (in landscape) and the table view's section index (if there is one).

I created a simple test app that adds a few dummy rows, a section header, and the section index.

Here is my code for creating a simple header view using a UILabel. My real app won't be using a label but a custom view.

let label = UILabel()
label.font = UIFont.systemFont(ofSize: 30)
label.backgroundColor = .green
label.text = "This is a really long Table Header label to make sure it is working properly."
label.sizeToFit()
tableView.tableHeaderView = label

Without any special attempts to fix the left and right edges, the result in the iPhone X simulator is as follows:

Portait:

enter image description here

Landscape:

enter image description here

Note that without any extra effort, the cells and section header get the desired insets but the header view does not.

I'd like the left edge of the header view to line up with the left edge of the section header and the cells.

I'd like the right edge of the header view to stop where it meets the left edge of the section index. Note that the portrait picture seems like it is already do that but if you look close you can tell the header view goes all the way to the right edge of the table view. You can't see the third . of the ellipses and you can barely see the green behind the section title view.

One attempt I've made was to add the following to my table view controller:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if let header = tableView.tableHeaderView {
        var insets = header.layoutMargins
        insets.left = tableView.layoutMargins.left
        insets.right = tableView.layoutMargins.right
        header.layoutMargins = insets
    }
}

That code has no effect.

What properties do I set to ensure the header view's left and right edges are indented as needed? Are there constraints that should be applied?

Please note that I'm doing everything in code. So please don't post any solutions that require storyboards or xib files. Answers in either Swift or Objective-C are welcome.


For anyone that wants to replicate this, create a new single view project. Adjust the main storyboard to use a UITableViewController instead of a plan UIViewController and use the following for ViewController:

import UIKit

class ViewController: UITableViewController {

    // MARK: - UITableViewController methods

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 5
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

        cell.textLabel?.text = "Row \(indexPath.row)"
        cell.accessoryType = .disclosureIndicator

        return cell
    }

    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {

    }

    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return "Section Header"
    }

    override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
        let coll = UILocalizedIndexedCollation.current()

        return coll.sectionIndexTitles
    }

    override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
        return index
    }

    // MARK: - UIViewController methods

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.sectionIndexMinimumDisplayRowCount = 1

        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")

        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 30)
        label.backgroundColor = .green
        label.text = "This is a really long Table Header label to make sure it is working properly."
        label.sizeToFit()
        tableView.tableHeaderView = label
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        if let header = tableView.tableHeaderView {
            var insets = header.layoutMargins
            insets.left = tableView.layoutMargins.left
            insets.right = tableView.layoutMargins.right
            header.layoutMargins = insets
        }
    }
}
rmaddy
  • 314,917
  • 42
  • 532
  • 579

2 Answers2

3

You need to add some Auto Layout constraints to the label after you add it to the tableview:

…
tableView.tableHeaderView = label
//Add this
label.translatesAutoresizingMaskIntoConstraints = false
label.leadingAnchor.constraint(equalTo: (label.superview?.safeAreaLayoutGuide.leadingAnchor)!).isActive = true
label.trailingAnchor.constraint(equalTo: (label.superview?.safeAreaLayoutGuide.trailingAnchor)!).isActive = true

Also, if you want to see all the text in the label use label.numberOfLines = 0.

You can get rid of the code you added to viewDidLayoutSubviews.

Update:

For fun I did some experimenting in a playground and found that using the layoutMarginsGuide didn't push the trailing edge of the header label far enough over (I'm thinking it comes out right on iPhone X but maybe not on all devices, or the playground behaves a bit differently). I did find though that for table views with at least one cell already in place I could use aCell.contentView.bounds.width, subtract the table view's width and divide the result by two and the header label would sit very nicely next to the section index. As a result I wrote a helper function for setting up constraints. The table view is optional so the function can be used with any view that has a superview and needs to keep inside the safe area. If a table view is passed in it can have a section index or not but it does need to have at least one cell at row 0 section 0:

func constrain(view: UIView, inTableView aTableView: UITableView?) {
    guard let container = view.superview else {
        print("Constrain error! View must have a superview to be constrained!!")
        return
    }
    view.translatesAutoresizingMaskIntoConstraints = false
    view.leadingAnchor.constraint(equalTo: container.safeAreaLayoutGuide.leadingAnchor).isActive = true
    if let table = aTableView, let aCell = table.cellForRow(at: IndexPath(row: 0, section: 0)) {
        let tableWidth = table.bounds.width
        let cellWidth = aCell.contentView.bounds.width
        view.trailingAnchor.constraint(equalTo: table.safeAreaLayoutGuide.trailingAnchor, constant: cellWidth - tableWidth).isActive = true
    } else {
        view.trailingAnchor.constraint(equalTo: container.safeAreaLayoutGuide.trailingAnchor).isActive = true
    }
}

I did find one issue when using this. When using a label set to 0 lines with your text it covers the first section header and the first cell of that section. It takes a bit of scrolling to get them out from under the header too. Clipping the label to one line works out quite well though.

theMikeSwan
  • 4,739
  • 2
  • 31
  • 44
  • Setting the leading anchor based on the safe area layout guide is perfect. If the table view doesn't have a section index then your solution for the trailing anchor is what is needed. But for a table view with a section index, the safe area trailing anchor isn't correct. I then used the layoutMarginGuide for the trailing anchor and that's perfect when there is a section index. So the proper trailing anchor needs to be based on whether the table view is showing a section index or not. – rmaddy Oct 11 '17 at 20:51
3

For UITableViews without section index:

label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    label.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
    label.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])

For UITableViews with section index:

label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    label.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
    label.trailingAnchor.constraint(equalTo: tableView.layoutMarginsGuide.trailingAnchor)
])
joern
  • 27,354
  • 7
  • 90
  • 105
  • This isn't working for me. 1 - why is the code to create and apply the header view in `viewDidLayoutSubviews`? That belongs in `viewDidLoad`. 2 - your trailingAnchor leaves a gap if the table has no section index. 3 - The leading anchor leaves an undesired gap in all conditions except an iPhone X in landscape. – rmaddy Oct 11 '17 at 21:55
  • Seems, it was already too late for me yesterday ;-) As you already pointed out in the comment to theMikeSwan's answer, you have to set different trailing constraints depending on whether the table view has a section index or not. I removed my old answer and added that two options. I am aware that the question was already basically answered by theMikeSwan, but rather than having a poor answer I'd rather have this. I'll happily delete this answer if you think that it's just a duplicate and doesn't offer any value. – joern Oct 12 '17 at 08:10