ZbadhabitZ
ZbadhabitZ

Reputation: 2913

Drawing Rounded Corners Using UIBezierPath

I have a design element that I'm having trouble figuring out; hoping someone may be able to point me in the right direction. The element I am trying to build is like so;

Rounded Button With No Bottom Border

Effectively, it's a rounded rectangle with a stroke on the left, top, and right sides (the bottom should have no stroke).

I've dabbled in using the following code;

// Create the rounded rectangle
let maskPath = UIBezierPath(roundedRect: myView.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: 4.0, height: 4.0))

// Setup a shape layer
let shape = CAShapeLayer()

// Create the shape path
shape.path = maskPath.cgPath

// Apply the mask
myView.layer.mask = shape

Subsequently, I'm using the following to draw the stroke around the rect;

// Add border
let borderLayer = CAShapeLayer()
borderLayer.path = maskPath.cgPath
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = UIColor.white.cgColor
borderLayer.lineWidth = 2.0
borderLayer.frame = self.bounds
self.layer.addSublayer(borderLayer)

This results in the following image;

Rounded Rect with Stroke

I've not been able to figure out how to either remove the bottom stroke or draw the item using a UIBezierPath(), but rounding the corners in a way that would be identical to the rounded rect (I'm using another rounded rect in the same view for a different purpose, and the rounded corners would need to be identical).

Thanks!

Upvotes: 7

Views: 10252

Answers (2)

matt
matt

Reputation: 536027

Don't use a shape layer. Use a layer (or a view). Draw the UIBezierPath's path into it and stroke it, and then erase the bottom line by drawing it and stroking it with a .clear blend mode.

Result:

enter image description here

Code (modify as desired; I use here a clear UIView that draws the shape as its draw code):

let p = UIBezierPath(roundedRect: self.bounds,
                     byRoundingCorners: [.topLeft, .topRight],
                     cornerRadii: CGSize(width: 4.0, height: 4.0))
UIColor.white.setStroke()
p.stroke()
let p2 = UIBezierPath()
p2.move(to: CGPoint(x:0, y:self.bounds.height))
p2.addLine(to: CGPoint(x:self.bounds.width, y:self.bounds.height))
p2.lineWidth = 2
p2.stroke(with: .clear, alpha: 1)

EDIT Another way would have been to clip out the bottom line area before drawing the rounded rect:

let p1 = UIBezierPath(rect: CGRect(origin:.zero,
                                   size:CGSize(width:self.bounds.width, height:self.bounds.height-2)))
p1.addClip()
let p = UIBezierPath(roundedRect: self.bounds,
                     byRoundingCorners: [.topLeft, .topRight],
                     cornerRadii: CGSize(width: 4.0, height: 4.0))
UIColor.white.setStroke()
p.stroke()

Upvotes: 6

rob mayoff
rob mayoff

Reputation: 386038

The CGMutablePath method addArc(tangent1End:tangent2End:radius:transform:) is designed to make round corners easily.

extension CGMutablePath {
    static func bottomlessRoundedRect(in rect: CGRect, radius: CGFloat) -> CGMutablePath {
        let path = CGMutablePath()
        path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
        path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.maxX, y: rect.minY), radius: radius)
        path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY), tangent2End: CGPoint(x: rect.maxX, y: rect.maxY), radius: radius)
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        return path
    }
}

Once you have that method, it's best to use a custom view to manage the CAShapeLayer so it can adapt to size changes automatically. Demo:

class MyFrameView: UIView {
    override class var layerClass: AnyClass { return CAShapeLayer.self }

    override func layoutSubviews() {
        super.layoutSubviews()
        let layer = self.layer as! CAShapeLayer
        layer.lineWidth = 2
        layer.strokeColor = UIColor.white.cgColor
        layer.fillColor = nil
        layer.path = CGMutablePath.bottomlessRoundedRect(in: bounds.insetBy(dx: 10, dy: 10), radius: 8)
    }
}

import PlaygroundSupport

let view = UIView(frame: CGRect(x: 0, y: 0, width: 120, height: 60))
view.backgroundColor = #colorLiteral(red: 0.7034167647, green: 0.4845994711, blue: 0.6114708185, alpha: 1)
let frameView = MyFrameView(frame: view.bounds)
frameView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(frameView)
let label = UILabel(frame: view.bounds)
label.text = "Hello"
label.textColor = .white
label.textAlignment = .center
view.addSubview(label)

PlaygroundPage.current.liveView = view

Result:

demo

Upvotes: 10

Related Questions