Rajeev Kulariya
Rajeev Kulariya

Reputation: 21

Inverted Corner Radius on all four corners of a UIView

I want to create a UIView that has a reversed/inverted corner radius on all four corners like this:

inverted rounded corners.

I've tried looking for solutions here but could not find what I needed. I would very much appreciate the help.

Upvotes: 1

Views: 2184

Answers (2)

Guilherme Carvalho
Guilherme Carvalho

Reputation: 330

I did a SwiftUI version based on Rob's response

 struct TicketShape: Shape {
  let cornerRadius: CGFloat

  func path(in rect: CGRect) -> Path {
    var path = Path()
    path.move(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY))
    path.addArc(center: CGPoint(x: rect.maxX, y: rect.minY),
                radius: cornerRadius, startAngle: .degrees(180),
                endAngle: .degrees(180/2),     clockwise: true)
    path.addArc(center: CGPoint(x: rect.maxX, y: rect.maxY),
                radius: cornerRadius, startAngle: .degrees(180 * 3/2),
                endAngle: .degrees(180),     clockwise: true)
    path.addArc(center: CGPoint(x: rect.minX, y: rect.maxY),
                radius: cornerRadius, startAngle: .degrees(0),
                endAngle: .degrees(180 * 3 / 2),     clockwise: true)
    path.addArc(center: CGPoint(x: rect.minX, y: rect.minY),
                radius: cornerRadius, startAngle: .degrees(180/2),
                endAngle: .degrees(0),     clockwise: true)

    return path
  }
}

Upvotes: 1

Rob
Rob

Reputation: 438047

You can create a UIView subclass that defines its backing layer to be a CAShapeLayer, configure that shape layer, and then respond to layoutSubviews, updating the path associated with that shape layer in such a way that it updates dynamically as the size of the view changes (e.g. maybe you have constraints that change the size based upon the dimensions of the device in question):

@IBDesignable
class InvertedCornerView: UIView {
    // define backing layer and provide computed property to make it easier to interact with

    override class var layerClass: AnyClass { return CAShapeLayer.self }
    private var shapeLayer: CAShapeLayer    { return layer as! CAShapeLayer }

    // a few public properties that dictate what it looks like
    // these are inspectable in case you want to show/customize this right in IB

    @IBInspectable var lineWidth: CGFloat = 5        { didSet { setNeedsLayout() } }
    @IBInspectable var cornerRadius: CGFloat = 20    { didSet { setNeedsLayout() } }
    @IBInspectable var strokeColor: UIColor = .black { didSet { shapeLayer.strokeColor = strokeColor.cgColor } }
    @IBInspectable var fillColor: UIColor = .clear   { didSet { shapeLayer.fillColor = fillColor.cgColor } }

    // basic lifecycle methods

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        configure()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        configure()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        updatePath()
    }
}

// MARK: - Private utility methods

private extension InvertedCornerView {
    func configure() {
        shapeLayer.strokeColor = strokeColor.cgColor
        shapeLayer.fillColor = fillColor.cgColor
    }

    func updatePath() {
        let rect = bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
        let path = UIBezierPath()
        path.move(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY))
        path.addArc(withCenter: CGPoint(x: rect.maxX, y: rect.minY), radius: cornerRadius, startAngle: .pi,         endAngle: .pi / 2,     clockwise: false)
        path.addArc(withCenter: CGPoint(x: rect.maxX, y: rect.maxY), radius: cornerRadius, startAngle: .pi * 3 / 2, endAngle: .pi,         clockwise: false)
        path.addArc(withCenter: CGPoint(x: rect.minX, y: rect.maxY), radius: cornerRadius, startAngle: 0,           endAngle: .pi * 3 / 2, clockwise: false)
        path.addArc(withCenter: CGPoint(x: rect.minX, y: rect.minY), radius: cornerRadius, startAngle: .pi / 2,     endAngle: 0,           clockwise: false)
        path.close()

        shapeLayer.path = path.cgPath
        shapeLayer.lineWidth = lineWidth
    }
}

That yields:

enter image description here

There are lots of variations on the theme (it looks good with cubic beziers for the corners, too), but hopefully this illustrates the basic idea. The heart of the idea is to use a CAShapeLayer whose path is updated by layoutSubviews.

Upvotes: 3

Related Questions