Reputation: 13
I'm trying to build a UITableView with cells that have rounded corners, borders and are indented. With these three combined, I have difficulty getting a 1.0pt border on the sides and between in the cells for each case (1 cell, 2 cells, > 2 cells).
In the simplest version, the only problem is that the borders between the cells are double width. Since the borderWidth
can only be set for the entire frame, I've tried to add individual borders with this:
extension CALayer {
func addBorder(edge: UIRectEdge, color: UIColor, thickness: CGFloat) {
let border = CALayer()
switch edge {
case .top:
border.frame = CGRect(x: 0, y: 0, width: frame.width, height: thickness)
case .bottom:
border.frame = CGRect(x: 0, y: frame.height - thickness, width: frame.width, height: thickness)
case .left:
border.frame = CGRect(x: 0, y: 0, width: thickness, height: frame.height)
case .right:
border.frame = CGRect(x: frame.width - thickness, y: 0, width: thickness, height: frame.height)
default:
break
}
border.backgroundColor = color.cgColor;
addSublayer(border)
}
}
When adding the borders individually with the addBorder
approach, other problem occur like:
Moreover, the type of problem also depends on whether the view is directly loaded (after re-running the app) or after adding / removing a cell.
clipToBounds
is set to true in the IB and the ContentMode of the ContentView is set to center.
I have subclassed UITableViewCell
as follows:
enum RoundedTableViewCellType {
case first
case last
case single
case middle
}
class RoundedTableViewCell: UITableViewCell {
override var frame: CGRect {
get {
return super.frame
}
set {
let inset: CGFloat = 20
var frame = newValue
frame.origin.x += inset
frame.size.width -= 2 * inset
super.frame = frame
}
}
var type: RoundedTableViewCellType = .middle {
didSet {
switch type {
case .first:
layer.cornerRadius = 6
layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
case .last:
layer.cornerRadius = 6
layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
case .single:
layer.cornerRadius = 6
layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner, .layerMinXMinYCorner, .layerMaxXMinYCorner]
case .middle:
layer.cornerRadius = 0
layer.maskedCorners = []
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
layer.borderColor = UIColor.primaryTransparant.cgColor
layer.borderWidth = 1.0
}
}
To clarify:
desired result
actual result
Many thanks!
Upvotes: 1
Views: 3262
Reputation: 77433
Using a typical UITableView
, this was achieved:
by adding these lines in viewDidLoad()
:
myTableView.layer.borderColor = myTableView.separatorColor?.cgColor
myTableView.layer.borderWidth = 1.0
myTableView.layer.cornerRadius = 6.0
Edit: Another approach, accommodating the use of frame
override to give the table "insets".
Add a CAShapeLayer
as a sub-layer to the cell. Set its path to a UIBezierPath
that forms the correct edges and corners.
Top cell will have only left, top and right edges (no bottom), with top corners rounded.
Middle cells will have left, top and right edges (no bottom), with NO corners rounded.
Bottom cell will have all 4 edges, with bottom corners rounded.
A cell in a one-row table will have all 4 edges, with all 4 corners rounded.
Result:
Complete code (no IBOutlets needed):
enum RoundedTableViewCellType {
case first
case last
case single
case middle
}
class RoundedTableViewCell: UITableViewCell {
var theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.font = UIFont.systemFont(ofSize: 16.0, weight: .bold)
v.textColor = UIColor(red: 62.0 / 255.0, green: 43.0 / 255.0, blue: 191.0 / 255.0, alpha: 1.0)
return v
}()
private var borderLayer = CAShapeLayer()
private var myType: RoundedTableViewCellType = .middle
override var frame: CGRect {
get {
return super.frame
}
set {
let inset: CGFloat = 20
var frame = newValue
frame.origin.x += inset
frame.size.width -= 2 * inset
super.frame = frame
}
}
var borderColor: UIColor = .clear {
didSet {
borderLayer.strokeColor = borderColor.cgColor
}
}
var borderWidth: CGFloat = 0.0 {
didSet {
borderLayer.lineWidth = borderWidth
}
}
// need to re-set layer cornerRadius if radius is set *after* type (in VC's cellForRowAt)
var radius: CGFloat = 6.0 {
didSet {
type = myType
}
}
var type: RoundedTableViewCellType = .middle {
didSet {
myType = type
switch type {
case .first:
layer.cornerRadius = radius
layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
case .last:
layer.cornerRadius = radius
layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
case .single:
layer.cornerRadius = radius
layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner, .layerMinXMinYCorner, .layerMaxXMinYCorner]
case .middle:
layer.cornerRadius = 0
layer.maskedCorners = []
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
let r = radius
var bPath = UIBezierPath()
let ptTopLeft = CGPoint(x: 0.0, y: 0.0)
let ptTopRight = CGPoint(x: bounds.width, y: 0.0)
let ptBotRight = CGPoint(x: bounds.width, y: bounds.height)
let ptBotLeft = CGPoint(x: 0.0, y: bounds.height)
switch type {
case .first:
// top cell, add left, top and right edges
// round top corners
bPath.move(to: ptBotLeft)
bPath.addLine(to: CGPoint(x: ptTopLeft.x, y: ptTopLeft.y + r))
bPath.addQuadCurve(to: CGPoint(x: ptTopLeft.x + r, y: ptTopLeft.y),
controlPoint: ptTopLeft)
bPath.addLine(to: CGPoint(x: ptTopRight.x - r, y: ptTopRight.y))
bPath.addQuadCurve(to: CGPoint(x: ptTopRight.x, y: ptTopRight.y + r),
controlPoint: ptTopRight)
bPath.addLine(to: CGPoint(x: ptBotRight.x, y: ptBotRight.y))
case .last:
// bottom cell, add all four edges
// round bottom corners
bPath = UIBezierPath(roundedRect: bounds,
byRoundingCorners: [.bottomLeft, .bottomRight],
cornerRadii: CGSize(width: r, height: r))
case .single:
// one-row table, add all four edges
// round all four corners
bPath = UIBezierPath(roundedRect: bounds, cornerRadius: r)
case .middle:
// middle cell, add left, top, right edges
// round NO corners
bPath.move(to: ptBotLeft)
bPath.addLine(to: ptTopLeft)
bPath.addLine(to: ptTopRight)
bPath.addLine(to: ptBotRight)
}
borderLayer.path = bPath.cgPath
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0),
theLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0),
theLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0),
theLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16.0),
])
layer.addSublayer(borderLayer)
borderLayer.fillColor = UIColor.clear.cgColor
// default values
borderColor = UIColor(red: 220.0 / 255.0, green: 215.0 / 255.0, blue: 244.0 / 255.0, alpha: 1.0)
borderWidth = 1.0
}
}
class RoundedCornersInsetTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var myTableView: UITableView = {
let v = UITableView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var theData = [1, 2, 3, 4]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 242.0 / 255.0, green: 240.0 / 255.0, blue: 250.0 / 255.0, alpha: 1.0)
myTableView.dataSource = self
myTableView.delegate = self
myTableView.register(RoundedTableViewCell.self, forCellReuseIdentifier: "RoundedTableViewCell")
myTableView.backgroundColor = .clear
myTableView.separatorStyle = .none
myTableView.tableFooterView = UIView(frame: CGRect.zero)
view.addSubview(myTableView)
NSLayoutConstraint.activate([
// constrain top + 40-pts
myTableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
// constrain leading / trailing to 0.0
myTableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0.0),
myTableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0.0),
// change this as appropriate
myTableView.heightAnchor.constraint(equalToConstant: 400.0)
])
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RoundedTableViewCell", for: indexPath) as! RoundedTableViewCell
cell.theLabel.text = "Cell \(theData[indexPath.row])"
cell.accessoryType = .disclosureIndicator
if theData.count == 1 {
cell.type = .single
} else {
if indexPath.row == 0 {
cell.type = .first
} else if indexPath.row == theData.count - 1 {
cell.type = .last
} else {
cell.type = .middle
}
}
// configurable cell border properties
//cell.borderColor = UIColor(red: 220.0 / 255.0, green: 215.0 / 255.0, blue: 244.0 / 255.0, alpha: 1.0)
//cell.borderWidth = 2.0
//cell.radius = 16.0
return cell
}
}
Upvotes: 5
Reputation: 1396
Issue is with border when setting border to 1 pixel frame takes 1 pixel from all four sides for first cell and when it creates second cell it again takes 1 pixel from all four sides Now on top border of 2nd cell combine with bottom border of 1st cell which actually appears like 2 pixel of thickness you need to handle this.
extension UIView {
// Example use: myView.addBorder(toSide: .Left, withColor: UIColor.redColor().CGColor, andThickness: 1.0)
enum ViewSide {
case Left, Right, Top, Bottom
}
func addBorder(toSide side: ViewSide, withColor color: CGColor, andThickness thickness: CGFloat) {
let border = CALayer()
border.backgroundColor = color
switch side {
case .Left: border.frame = CGRect(x: frame.minX, y: frame.minY, width: thickness, height: frame.height); break
case .Right: border.frame = CGRect(x: frame.maxX, y: frame.minY, width: thickness, height: frame.height); break
case .Top: border.frame = CGRect(x: frame.minX, y: frame.minY, width: frame.width, height: thickness); break
case .Bottom: border.frame = CGRect(x: frame.minX, y: frame.maxY, width: frame.width, height: thickness); break
}
layer.addSublayer(border)
}
}
1- For first cell add border drop all sides
2- and for others add borders except Top border.
Upvotes: 0