Home > front end >  tableHeaderView is overlapping cells when adding custom view to subview of container view
tableHeaderView is overlapping cells when adding custom view to subview of container view

Time:11-25

I am currently using a UIViewController and adding a UITableView to the view. With this tableView I am adding a UIView called containerView to its tableHeaderView. I set the height of the container view and then adding a second UIView to its subview, that is pinned to the bottom of the containerView.

When I add it to the header view the cells are being overlapped. What's odd though is that if I don't add the subview to the container view the headerView is not being overlapped by the cells, it is only occurring when I am adding the second view as a subview to the container view.

class ViewController: UIViewController {

    private var containerView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.alpha = 0.7
        view.backgroundColor = .red
        return view
    }()

    private var bottomView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .blue
        return view
    }()

    private(set) lazy var tableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.dataSource = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        return tableView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(tableView)

        containerView.addSubview(bottomView)
        tableView.tableHeaderView = containerView

        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

            containerView.topAnchor.constraint(equalTo: tableView.topAnchor),
            containerView.heightAnchor.constraint(equalToConstant: 214),
            containerView.widthAnchor.constraint(equalToConstant: view.frame.size.width),

            bottomView.topAnchor.constraint(equalTo: containerView.bottomAnchor),
            bottomView.heightAnchor.constraint(equalToConstant: 114),
            bottomView.widthAnchor.constraint(equalToConstant: view.frame.size.width),
        ])
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.contentInset = UIEdgeInsets(top: -view.safeAreaInsets.top, left: 0, bottom: 0, right: 0)
        tableView.tableHeaderView?.autoresizingMask = []
        tableView.tableHeaderView?.layoutIfNeeded()
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(true)
    }
} 

enter image description here

CodePudding user response:

The reason your "blue view" is overlapping the cells is because you are constraining its Top to the red view's Bottom, but you're not updating the header view size.

One good approach is to create a UIView subclass to use as your header view. Setup all of its content with proper auto-layout constraints.

Then, in the controller's viewDidLayoutSubviews(), we use .systemLayoutSizeFitting(...) to determine the header view's height and update its frame:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    // update table header size

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

Here is a complete example...

First, our custom view class:

class SampleHeaderView: UIView {

    let redView: UIView = {
        let v = UIView()
        v.backgroundColor = .systemRed
        return v
    }()
    let blueView: UIView = {
        let v = UIView()
        v.backgroundColor = .systemBlue
        return v
    }()
    let redTopLabel: UILabel = {
        let v = UILabel()
        v.backgroundColor = .yellow
        v.numberOfLines = 0
        return v
    }()
    let redBottomLabel: UILabel = {
        let v = UILabel()
        v.backgroundColor = .green
        v.numberOfLines = 0
        return v
    }()
    let multiLineLabel: UILabel = {
        let v = UILabel()
        v.backgroundColor = .cyan
        v.numberOfLines = 0
        return v
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    func commonInit() -> Void {
        
        // all views will use auto-layout
        [redView, blueView, redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        
        // prevent label vertical compression
        [redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
            v.setContentCompressionResistancePriority(.required, for: .vertical)
        }
        
        // add top and bottom labels to red view
        redView.addSubview(redTopLabel)
        redView.addSubview(redBottomLabel)

        // add multi-line label to blue view
        blueView.addSubview(multiLineLabel)
        
        // add red and blue views to self
        addSubview(redView)
        addSubview(blueView)

        // the following constraints need to have less-than required to avoid
        //  auto-layout warnings
        
        // blue view bottom to self
        let c1 = blueView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
        
        // labels trailing contraints
        let c2 = redTopLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
        let c3 = redBottomLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
        let c4 = multiLineLabel.trailingAnchor.constraint(equalTo: blueView.trailingAnchor, constant: -8.0)
        
        [c1, c2, c3, c4].forEach { c in
            c.priority = .required - 1
        }
        
        NSLayoutConstraint.activate([
            
            // red view top to self
            redView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),

            // leading / trailing to self
            redView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
            redView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),

            // blue view top to red view bottom
            blueView.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 0.0),

            //  leading / trailing to self
            blueView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
            blueView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
            
            // top and bottom labels, constrained in red view
            //  with a little "padding"
            redTopLabel.topAnchor.constraint(equalTo: redView.topAnchor, constant: 8.0),
            redTopLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),

            redBottomLabel.topAnchor.constraint(equalTo: redTopLabel.bottomAnchor, constant: 8.0),
            redBottomLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),

            redBottomLabel.bottomAnchor.constraint(equalTo: redView.bottomAnchor, constant: -8.0),

            // multi-line label, constrained in blue view
            //  with a little "padding"
            multiLineLabel.topAnchor.constraint(equalTo: blueView.topAnchor, constant: 8.0),
            multiLineLabel.leadingAnchor.constraint(equalTo: blueView.leadingAnchor, constant: 8.0),
            multiLineLabel.bottomAnchor.constraint(equalTo: blueView.bottomAnchor, constant: -8.0),

            // the less-than-required priority constraints
            c1, c2, c3, c4,

        ])
        
    }
}

and a sample controller:

class TableHeaderViewController: UIViewController {
    
    var sampleHeaderView = SampleHeaderView()
    
    private(set) lazy var tableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.dataSource = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        return tableView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(tableView)
        
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])

        sampleHeaderView.redTopLabel.text = "The Red Top Label"
        sampleHeaderView.redBottomLabel.text = "The Red Bottom Label, with enough text that is should wrap."
        sampleHeaderView.multiLineLabel.text = "This text is for the Label in the Blue View. It is also long enough that it will require word-wrapping. Note that the header updates itself when the frame changes, such as on device rotation."
        tableView.tableHeaderView = sampleHeaderView

    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        // update table header size

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

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

Output:

enter image description here

and rotated:

enter image description here

  • Related