Search code examples
iosuiviewcalayer

UITableViewCell shadows overlay


I have a table view with cells. Overlaying shadows is done, but that looks not like I wanted. My shadow white round rectangles should stay white. And shadows should overlay below white rectangles. Any suggestions on how to achieve expected behavior?

enter image description here

I added shadow as a separate subview


class ShadowView: UIView {
    
    override var bounds: CGRect {
        didSet {
            setupShadow()
        }
    }
    
    private func setupShadow() {
        layer.shadowColor = UIColor.red.cgColor
        layer.shadowOpacity = 1
        layer.shadowRadius = 40
        layer.shadowOffset = CGSize(width: 1, height: 10)
        layer.masksToBounds = false
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 5).cgPath
    }
}

and then

let shadowView = ShadowView()        
addSubview(shadowView)

I wanted something like this. White rectangles are completely white.

enter image description here


Solution

  • The problem, as you are seeing, is that rows (cells) are separate views. If you allow an element to extend outside the cell, it will either overlap or underlap the adjacent views.

    Here's a simple example to clarify...

    Each cell has a systemYellow view that extends outside its frame on the top and bottom:

    enter image description here

    If we use Debug View Hierarchy to inspect the layout, it looks something like this:

    enter image description here

    As we can see, because of the initial z-order, each cell is covering the part of the systemYellow view that is extending up and the part that is extending down overlaps the next cell.

    As we scroll a bit, cells are re-drawn at different z-order positions (based on how the tableView re-uses them):

    enter image description here

    Now we see that some of the systemYellow views overlap the row above, some overlap the row below, and some overlap both.

    Inspecting the layout shows us the cells' z-order positions:

    enter image description here

    If we want to maintain the z-order so that none of the systemYellow views overlap the cell below it, we can add a func to manipulate the z-order positions:

    func updateLayout() -> Void {
        for c in tableView.visibleCells {
            tableView.bringSubviewToFront(c)
        }
    }
    

    and we need to call that whenever the tableView scrolls (and when the layout changes):

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        updateLayout()
    }
    
    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        updateLayout()
    }
    

    So, the same thing is happening with your layout... the shadows are extending outside the frame of the cell, and either over- or under-lapping the adjacent cells.

    If we start by using the same approach to manage the z-order of the cells, we can get this:

    enter image description here

    enter image description here

    So, we're keeping the white rounded-rect views on top of the "shadow above." Of course, now we have the shadows overlapping the bottom of the view.

    We can change the rectangle for the .shadowPath to avoid that:

    override func layoutSubviews() {
        super.layoutSubviews()
        var r = bounds
        r.origin.y += 40
        layer.shadowPath = UIBezierPath(roundedRect: r, cornerRadius: 5).cgPath
    }
    

    and we get this output:

    enter image description here

    enter image description here

    One more issue though -- if we use the default cell .selectionStyle, we get this:

    enter image description here

    which is probably not acceptable.

    So, we can set the .selectionStyle to .none, and implement setSelected in our cell class. Here, I change the rounded-rect background and the text colors to make it extremely obvious:

    enter image description here

    Here is some example code -- no @IBOutlet or @IBAction connections needed, so just assign the class of a new table view controller to ShadowTableViewController :

    class ShadowView: UIView {
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            layer.shadowColor = UIColor.red.cgColor
            layer.shadowOpacity = 1
            layer.shadowRadius = 40
            layer.masksToBounds = false
            layer.cornerRadius = 12
            layer.shouldRasterize = true
        }
        override func layoutSubviews() {
            super.layoutSubviews()
            var r = bounds
            r.origin.y += 40
            layer.shadowPath = UIBezierPath(roundedRect: r, cornerRadius: 5).cgPath
        }
    
    }
    
    class ShadowCell: UITableViewCell {
        
        let shadowView = ShadowView()
        let topLabel = UILabel()
        let bottomLabel = UILabel()
        
        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 {
            
            shadowView.backgroundColor = .white
            
            topLabel.font = .boldSystemFont(ofSize: 24.0)
            bottomLabel.font = .italicSystemFont(ofSize: 20.0)
            bottomLabel.numberOfLines = 0
            
            let stack = UIStackView()
            stack.axis = .vertical
            stack.spacing = 8
            
            stack.addArrangedSubview(topLabel)
            stack.addArrangedSubview(bottomLabel)
            
            shadowView.translatesAutoresizingMaskIntoConstraints = false
            stack.translatesAutoresizingMaskIntoConstraints = false
            
            shadowView.addSubview(stack)
            contentView.addSubview(shadowView)
            
            let mg = contentView.layoutMarginsGuide
            
            NSLayoutConstraint.activate([
                
                shadowView.topAnchor.constraint(equalTo: mg.topAnchor),
                shadowView.leadingAnchor.constraint(equalTo: mg.leadingAnchor),
                shadowView.trailingAnchor.constraint(equalTo: mg.trailingAnchor),
                shadowView.bottomAnchor.constraint(equalTo: mg.bottomAnchor),
                
                stack.topAnchor.constraint(equalTo: shadowView.topAnchor, constant: 12.0),
                stack.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor, constant: 12.0),
                stack.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -12.0),
                stack.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: -12.0),
                
            ])
            
            contentView.clipsToBounds = false
            self.clipsToBounds = false
            self.backgroundColor = .clear
    
            selectionStyle = .none
        }
    
        override func setSelected(_ selected: Bool, animated: Bool) {
            super.setSelected(selected, animated: animated)
            shadowView.backgroundColor = selected ? .systemBlue : .white
            topLabel.textColor = selected ? .white : .black
            bottomLabel.textColor = selected ? .white : .black
        }
    
    }
    
    class ShadowTableViewController: UITableViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            tableView.separatorStyle = .none
            tableView.register(ShadowCell.self, forCellReuseIdentifier: "shadowCell")
        }
    
        func updateLayout() -> Void {
            for c in tableView.visibleCells {
                tableView.bringSubviewToFront(c)
            }
        }
    
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            updateLayout()
        }
        override func scrollViewDidScroll(_ scrollView: UIScrollView) {
            updateLayout()
        }
        
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return 30
        }
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: "shadowCell", for: indexPath) as! ShadowCell
            c.topLabel.text = "Row: \(indexPath.row)"
            var s = "Description for row \(indexPath.row)"
            if indexPath.row % 3 == 1 {
                s += "\nSecond Line"
            }
            if indexPath.row % 3 == 2 {
                s += "\nSecond Line\nThirdLine"
            }
            c.bottomLabel.text = s
            return c
        }
        
    }
    

    Note: this is Example Code Only and should not be considered Production Ready.