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?
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.
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:
If we use Debug View Hierarchy
to inspect the layout, it looks something like this:
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):
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:
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:
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:
One more issue though -- if we use the default cell .selectionStyle
, we get this:
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:
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.