简体   繁体   中英

How to make an indented UITableViewCell with rounded corners and single line borders

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:

  • The border on the right side isn't visible because the new frame width that is set for the indention isn't taken into account
  • The corners aren't rounded correctly
  • There's still a double width separator

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!

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
    }

}

在此处输入图片说明 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM