iOS How to achieve my drawing using a bezier path?

390 views Asked by At

I need to have a border and corner radius on the section of my collectionView. So if it's the first row I need to draw a bottomless rect layer. And if it's the last row I will draw a topless layer.

How to achieve my drawing using a bezier path?

enter image description here

Edited

I have used this code, it works. But I could not create a method for topless rect.

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
 }
}

let layer = CAShapeLayer()
layer.lineWidth = 1
layer.strokeColor = UIColor.black.cgColor
layer.fillColor = nil
layer.path = CGMutablePath.bottomlessRoundedRect(in: testView.bounds.insetBy(dx: 1, dy: 1), radius: 18)
view.layer.insertSublayer(layer, at: 0)

view.layoutIfNeeded()

Here is method for topless rect, it is not working correctly.

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

    path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY), tangent2End: CGPoint(x: rect.maxX, y: rect.minY), radius: radius)
    path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
    return path
}

This is the result. enter image description here

Please help me to have a correct topless rect. (I have not used drawing before)

The answer

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

    path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.maxX, y: rect.minY), radius: radius)
    path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
    return path
}
2

There are 2 answers

1
Duncan C On

Diagram it out. Rounded rectangles are a series of line segments connected to quarter-circle arcs of a given radius. (the corner radius.)

iOS uses different coordinate systems depending on how you're doing your drawing (Core Graphics uses LLO, or lower left orgin, and UIKit/Core Animation uses ULO, or upper left origin.)

It looks like the CGPaths used in CAShapeLayers use ULO (upper-left-origin) coordinates, where 0,0 is in the upper left corner and Y increases as you go down.

Your bottomlessRoundedRect() function starts with this line of code:

path.move(to: CGPoint(x: rect.minX, y: rect.maxY))

That moves to the lower left (max Y) position of the rectangle to start.

Annotating the whole function, here's what it does:

static func bottomlessRoundedRect(in rect: CGRect, radius: CGFloat) -> CGMutablePath {
    let path = CGMutablePath()
    //Move to the lower left corner of the rect (starting point)
    path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
    
    //Draw a line from the starting point to the beginning of the arc in the
    //top left corner, and draw the top left rounded corner
    path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY),
                tangent2End: CGPoint(x: rect.maxX, y: rect.minY),
                radius: radius)
    
    //Draw a line from the top left corner to the begnning of the top right
    //arc, and the top right corner arc
    path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY),
                tangent2End: CGPoint(x: rect.maxX, y: rect.maxY),
                radius: radius)
    
    //Draw a final line from the end of the top right corner arc to
    //the bottom right corner
    path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
    return path
}

Draw your full rectangle. Pick your corner radius. Draw an inner rectangle who's corners are inset by the corner radius. Draw circles at the corners of your inner rectangle, and note how they meet the sides of your outer rectangles.

Now put it together.

Open a bezier path. Move to your desired starting point. Create a line to the next side, minus your corner radius. Draw a 1/4 circle arc starting and ending at the angles needed to complete that corner. Draw the next line segment. Draw another arc. Draw your final line segment.

An annotated version of a toplessRoundedRect() function looks like this:

static func toplessRoundedRect(in rect: CGRect, radius: CGFloat) -> CGMutablePath {
    let path = CGMutablePath()
    
    //Move to the top left corner.
    path.move(to: CGPoint(x: rect.minX, y: rect.minY))
    
    //Draw a line from the top left corner to the begnning of the bottom left
    //rounded corner, plus the bottom left rounded corner
    path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY),
                tangent2End: CGPoint(x: rect.maxX, y: rect.maxY),
                radius: radius)
    
    //Draw a line from the end of the bottom left rounded corner to the beginning
    //of the bottom right rounded corner, plus the bottom right rounded corner
    path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY),
                tangent2End: CGPoint(x: rect.maxX, y: rect.minY),
                radius: radius)
    
    //Draw a line from the end of the bottom right rounded corner
    //to the top right corner.
    path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
    return path
}
1
DonMag On

Here's a tip: work through your code one line at a time.

Change your CGMutablePath extension to this:

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

When you run your app, only the 1 part of the path will be executed. You won't see anything, because all you've done so far is move to a point.

Now, uncomment the 2 part. Run your code, and see what the result is.

Uncomment the 3 part. Run your code, and see what the result is.

Uncomment the 4 part. Run your code, and see what the result is.

By now, you should have a pretty good idea of what the code is doing, and creating your toplessRoundedRect func should be a piece of cake.