Aggressor
Aggressor

Reputation: 13551

UITableViewCell Borders Change When tableView.reloadData() is called

Very confused about what is happening with my UITableViewCell when reloadData is called. When the tableview first loads, each cell has its correct rounding. The top has its top corners rounded, the middle has no rounding, and the bottom has only its bottom.

Here is what it looks like on first load: First Load Cells

Here is what happens if a tableview.reloadData is called (1 or more times) Subsequent reloads

Here is the code where the cells are created (in func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath))

        guard let cell = tableView.dequeueReusableCell(withIdentifier: MyCell().reuseIdentifier, for: indexPath) as? MyCell else { return UITableViewCell() }

        if indexPath.row == 0 {
            cell.roundCorners(corners: (top: true, bottom: false))
        }
        if indexPath.row == 1 {
            cell.roundCorners(corners: (top: false, bottom: false))
        }
        if indexPath.row == 2 {
            cell.roundCorners(corners: (top: false, bottom: true))
        }

Here is the code for the rounded corners:

    func roundCorners(corners: (top: Bool, bottom: Bool)) {
        if !corners.top && !corners.bottom {
            roundCorners(withRadius: 0)
        } else if corners.top && corners.bottom {
           roundCorners(withRadius: 20)
        } else if corners.top {
            roundCorners(corners: [.topLeft, .topRight], radius: 20)
        } else if corners.bottom {
            roundCorners(corners: [.bottomLeft, .bottomRight], radius: 20)
        }
    }

// and the actual rounding methods

    func roundCorners() {
        roundCorners(withRadius: bounds.size.height * 0.5)
    }

    func roundCorners(withRadius radius: CGFloat) {
        layer.cornerRadius = radius
        layer.masksToBounds = true
    }

    func roundCorners(corners: UIRectCorner, radius: CGFloat) {
        let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        let mask = CAShapeLayer()
        mask.path = path.cgPath
        layer.mask = mask
    }

    func maskedCorners(withRadius radius: CGFloat) {
        layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
        layer.cornerRadius = radius
        clipsToBounds = true
    }

    func maskedCorners(corners: CACornerMask, radius: CGFloat) {
        layer.maskedCorners = corners
        layer.cornerRadius = radius
        clipsToBounds = true
    }

I have tried several things which did not work.

  1. Instead of reusing cells (I thought maybe the way they were being reused was mucking with the draw calls), I tried instantiating the cell each time

  2. I tried resetting the cell borders to 0 and then setting which corners should be rounded

  3. I tried calling the .setNeedsDisplay() after updating the borders

I will also note that if I comment out the rounding calls, no rounding occurs at all, so the issue is strictly isolated to the code above.

Very open to suggestions as to why after the first reloadData() call it rounds the middle cell and keeps it rounded moving forward.

Upvotes: 1

Views: 190

Answers (3)

Mohammad Zmy
Mohammad Zmy

Reputation: 1

You can round corners of the tableView:

tableView.layer.cornerRadius = 20
tableView.clipsToBounds = true

Upvotes: 0

DonMag
DonMag

Reputation: 77637

Couple issues...

1 - you're using bounds before the cell's layout is complete, so UIBezierPath(roundedRect: bounds, ...) can be wrong.

2 - in this code:

func roundCorners(corners: (top: Bool, bottom: Bool)) {
    if !corners.top && !corners.bottom {
        // NOT setting a layer mask
        roundCorners(withRadius: 0)
    } else if corners.top && corners.bottom {
        // NOT setting a layer mask
        roundCorners(withRadius: 20)
    } else if corners.top {
        // YES setting a layer mask
        roundCorners(corners: [.topLeft, .topRight], radius: 20)
    } else if corners.bottom {
        // YES setting a layer mask
        roundCorners(corners: [.bottomLeft, .bottomRight], radius: 20)
    }
}

cells are reused, so you can get a cell used for the First or Last row - where the layer mask is set - then it gets used as a "middle" row and the layer mask is not cleared.

So, either clear the mask for the "middle" rows (layer.mask = nil), or always call the func that uses the mask:

func roundCorners(corners: (top: Bool, bottom: Bool)) {
    if !corners.top && !corners.bottom {
        roundCorners(corners: [], radius: 0)
    } else if corners.top && corners.bottom {
        roundCorners(corners: [], radius: 20)
    } else if corners.top {
        roundCorners(corners: [.topLeft, .topRight], radius: 20)
    } else if corners.bottom {
        roundCorners(corners: [.bottomLeft, .bottomRight], radius: 20)
    }
}

Edit

Here's some sample code that you might find makes things a bit more manageable...

Cell Class

class RoundedCornersCell: UITableViewCell {
    
    enum CornerStyle {
        case solo, top, middle, bottom
    }
    
    public var cornerStyle: CornerStyle = .middle {
        didSet {
            setNeedsLayout()
        }
    }
    public var cornerRadius: CGFloat = 20.0 {
        didSet {
            setNeedsLayout()
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        var theCorners: UIRectCorner = []
        switch cornerStyle {
        case .solo:
            theCorners = .allCorners
        case .top:
            theCorners = [.topLeft, .topRight]
        case .bottom:
            theCorners = [.bottomLeft, .bottomRight]
        case .middle:
            ()
        }
        
        let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: theCorners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
        let mask = CAShapeLayer()
        mask.path = path.cgPath
        layer.mask = mask
    }

}

Example Controller Class

class ExampleTableVC: UIViewController, UITableViewDataSource, UITableViewDelegate {

    let tableView = UITableView()
    
    var myData: [Int] = []
    var shuffledData: [Int]!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        view.backgroundColor = .systemBlue
        
        myData = Array(1...5)
        
        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(RoundedCornersCell.self, forCellReuseIdentifier: "rcc")
        tableView.dataSource = self
        tableView.delegate = self
        
        tableView.rowHeight = 60
        tableView.backgroundColor = .clear
        
        shuffledData = myData
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return shuffledData.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "rcc", for: indexPath) as! RoundedCornersCell
        
        // if there's only one row
        if tableView.numberOfRows(inSection: indexPath.section) == 1 {
            c.cornerStyle = .solo
        } else {
            c.cornerStyle =
            indexPath.row == 0 ? .top
            : indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 ? .bottom
            : .middle
        }

        c.textLabel?.text = "\(shuffledData[indexPath.row])"
        return c
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        shuffledData = myData.shuffled()
        tableView.reloadData()
    }
}

Upvotes: 1

Ali Hamza
Ali Hamza

Reputation: 181

For such layout you don't have to code instead you can use tableview style Inset Grouped.

Just go to your Storyboard settings > Table attribute inspector > Style > Select Inset Grouped:

table view settings

And here's the output:

output

Upvotes: 4

Related Questions