Reputation: 21
I want to create a UIView that has a reversed/inverted corner radius on all four corners like this:
.
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
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
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:
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