iosswiftuitableviewuikitnstableheaderview

tableHeaderView is overlapping cells when adding custom view to subview of container view


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


Solution

  • 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