Search code examples
iosswiftuitableviewuiviewuikit

How to add UIView above a tableView Swift(iOS)


I'm trying to add a UIview above the tableView but my view is hiding behind the cells. The view should half on footer of section 0 and half on header of section 1, The empty space is tha padding for header.

enter image description here

I have used bringSubviewToFront method but it's not working. Everything that I had tried are commented below.

    func setupMyRankView(){
//        tableView.addSubview(myRankView)

//      tableView.footerView(forSection: 0)?.insertSubview(myRankView, aboveSubview: self.view)
//        tableView.footerView(forSection: 0)?.bringSubviewToFront(myRankView)
                self.view.bringSubviewToFront(myRankView)
//        tableView.cellForRow(at: indexPath)?.insertSubview(myRankView, aboveSubview: self.view)
//        tableView.cellForRow(at: indexPath)?.bringSubviewToFront(myRankView)
        
       myRankView.myRankLabel.text = "Hello"
       
    }

Solution

  • As the table view manages its cells and section headers/footers, it is constantly rearranging the z-orders.

    What you need to do is bring the "rank" views to the front every time the table updates itself.

    One way to do that is by implementing the table view's scrollViewDidScroll -- this gives you the added advantage of being a good place to calculate the frame(s) for the "rank" view(s).

    It will go something like this:

    • When you load the table view, create an array of "rank" views and add them as subviews of the table view itself.
    • on scrollViewDidScroll
      • get the rect of the section footer view
      • get the rect of the next section header view
      • set the frame of the "rank" view so it is positioned between the sections
      • bring the "rank" view to the front

    and the code will look like this:

        // for each of the "rank" views
        for (i, v) in rankViews.enumerated() {
            // get the rect for the section footer
            var r1 = tableView.rectForFooter(inSection: i)
            // get the rect for the NEXT section header
            let r2 = tableView.rectForHeader(inSection: i + 1)
            
            // set the full rect size to the
            //  Bottom of the Header minus the Top of the Footer
            r1.size.height = r2.maxY - r1.minY
            
            // start with setting the frame of the "rank" view to 50x50
            v.frame = CGRect(x: 0.0, y: 0.0, width: 50.0, height: 50.0)
            // center it horizontonally and vertically
            v.center = CGPoint(x: r1.midX, y: r1.midY)
            
            // bring it to the front
            tableView.bringSubviewToFront(v)
        }
    

    Here's a complete example you can try out...

    Sample "Rank" view

    class MyRankView: UIView {
        
        let myRankLabel = UILabel()
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            myRankLabel.textAlignment = .center
            myRankLabel.translatesAutoresizingMaskIntoConstraints = false
            addSubview(myRankLabel)
            let g = self
            NSLayoutConstraint.activate([
                myRankLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                myRankLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                myRankLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                myRankLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            ])
            layer.borderColor = UIColor.systemGreen.cgColor
            layer.borderWidth = 2
            myRankLabel.textColor = .systemGreen
            backgroundColor = .white
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            layer.cornerRadius = min(bounds.width, bounds.height) * 0.5
        }
    }
    

    Simple multi-line label cell

    class MyTestCell: UITableViewCell {
        
        let theLabel: UILabel = {
            let v = UILabel()
            v.numberOfLines = 0
            return v
        }()
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            theLabel.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(theLabel)
            let g = contentView.layoutMarginsGuide
            NSLayoutConstraint.activate([
                theLabel.topAnchor.constraint(equalTo: g.topAnchor),
                theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            ])
            theLabel.backgroundColor = .yellow
        }
        
    }
    

    Reusable section header/footer views

    class MyHeaderFooterBaseView: UITableViewHeaderFooterView {
        
        let label = UILabel()
        override init(reuseIdentifier: String?) {
            super.init(reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            label.translatesAutoresizingMaskIntoConstraints = false
            addSubview(label)
            let g = self.layoutMarginsGuide
            NSLayoutConstraint.activate([
                label.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
                label.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                label.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
            ])
            backgroundView = UIView()
        }
    }
    
    class MyHeaderView: MyHeaderFooterBaseView {
        override func commonInit() {
            super.commonInit()
            label.textAlignment = .center
            backgroundView?.backgroundColor = .cyan
        }
    }
    
    class MyFooterView: MyHeaderFooterBaseView {
        override func commonInit() {
            super.commonInit()
            label.textAlignment = .left
            backgroundView?.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        }
    }
    

    Sample controller

    class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
        let tableView = UITableView(frame: .zero, style: .grouped)
        
        // we'll use varying
        //  number of rows per section and
        //  number of lines per row
        let myData: [[Int]] = [
            [1, 2, 3, 4],
            [3, 2, 1],
            [2, 1, 4, 3, 1],
            [2, 2, 2, 3],
            [1, 1, 5, 1],
            [2, 2, 2],
        ]
        
        var rankViews: [MyRankView] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.backgroundColor = .systemYellow
            
            tableView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(tableView)
            
            let g = view.safeAreaLayoutGuide
            
            NSLayoutConstraint.activate([
                
                tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
                
            ])
            
            tableView.register(MyTestCell.self, forCellReuseIdentifier: "c")
            tableView.register(MyHeaderView.self, forHeaderFooterViewReuseIdentifier: "h")
            tableView.register(MyFooterView.self, forHeaderFooterViewReuseIdentifier: "f")
            
            tableView.dataSource = self
            tableView.delegate = self
            
            tableView.sectionHeaderTopPadding = 20.0
            tableView.estimatedSectionHeaderHeight = 60.0
            tableView.estimatedSectionFooterHeight = 60.0
            
            for i in 0..<myData.count - 1 {
                let v = MyRankView()
                v.myRankLabel.text = "\(i)"
                tableView.addSubview(v)
                rankViews.append(v)
            }
    
        }
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            updateRankViews()
        }
        func numberOfSections(in tableView: UITableView) -> Int {
            return myData.count
        }
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return myData[section].count
        }
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! MyTestCell
            var s: String = "\(indexPath)"
            for i in 2..<(myData[indexPath.section][indexPath.row] + 1) {
                s += "\nLine \(i)"
            }
            c.theLabel.text = s
            return c
        }
        func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
            let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: "h") as! MyHeaderView
            v.label.text = "Section Header: \(section)"
            return v
        }
        func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
            let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: "f") as! MyFooterView
            v.label.text = "Section Footer: \(section)"
            return v
        }
        
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            updateRankViews()
        }
        func updateRankViews() {
            
            // for each of the "rank" views
            for (i, v) in rankViews.enumerated() {
                // get the rect for the section footer
                var r1 = tableView.rectForFooter(inSection: i)
                // get the rect for the NEXT section header
                let r2 = tableView.rectForHeader(inSection: i + 1)
                
                // set the full rect size to the
                //  Bottom of the Header minus the Top of the Footer
                r1.size.height = r2.maxY - r1.minY
                
                // start with setting the frame of the "rank" view to 50x50
                v.frame = CGRect(x: 0.0, y: 0.0, width: 50.0, height: 50.0)
                // center it horizontonally and vertically
                v.center = CGPoint(x: r1.midX, y: r1.midY)
                
                // bring it to the front
                tableView.bringSubviewToFront(v)
            }
            
        }
    }
    

    and it looks like this when running:

    enter image description here enter image description here