Search code examples
iosswiftuikituibezierpathcurve

How to draw a curve in Bezier Path?


I'm trying to achieve this design in iOS UIKit:

The mock image

Using bezier path I have completed it but I am unable to achieve the inverted curve from the left side.

I'm attaching the code snippet that I use:

class ArcView : UIView {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    func setupViewWith(textToDisplay:String,imageNamed:String,backgroundColor:UIColor){
        setupView(backgroundColor: backgroundColor)
    }
    
    func setupView(backgroundColor:UIColor){
        let criclepath : UIBezierPath = getCirclePath()
        let shapeLayer = CAShapeLayer()
        shapeLayer.position = CGPoint(x: 0, y: 0)
        shapeLayer.path = criclepath.cgPath
        shapeLayer.lineWidth = 1.5
        shapeLayer.strokeColor = UIColor.white.cgColor
        shapeLayer.fillColor = backgroundColor.cgColor
        self.layer.addSublayer(shapeLayer)
        
        let supportpath : UIBezierPath = getPath()
        let supportLayer = CAShapeLayer()
        supportLayer.path = supportpath.cgPath
        supportLayer.lineWidth = 1.5
        supportLayer.strokeColor = UIColor.white.cgColor
        supportLayer.fillColor = backgroundColor.cgColor
        self.layer.addSublayer(supportLayer)
    }
    
    func getPath()->UIBezierPath{
        let height = self.frame.size.height
        let width = self.frame.size.width
        let halfHeight = height/2.0
        let xPosition = 40.0
        let path = UIBezierPath()
        
        path.move(to: CGPoint(x: xPosition, y: 0))
        
        path.addLine(to: CGPoint(x: width-halfHeight, y: 0))
        
        path.addArc(withCenter: CGPoint(x: width-halfHeight, y: halfHeight),
                    radius: halfHeight,
                    startAngle: CGFloat(270).toRadians(),
                    endAngle: CGFloat(90).toRadians(),
                    clockwise: true)
        
        path.addLine(to: CGPoint(x: xPosition, y: height))
    
        path.addCurve(to: CGPoint(x: xPosition, y: 0),
                      controlPoint1: CGPoint(x: xPosition+25 , y: 30),
                      controlPoint2: CGPoint(x: xPosition+10, y: 5))
        path.close()
        
        return path
    }
    
    func getCirclePath()->UIBezierPath{
        
        let path = UIBezierPath(ovalIn: CGRect(x: 0,
                                               y: 0,
                                               width: self.frame.size.height,
                                               height: self.frame.size.height))
        path.close()
        return path
    }
}

extension CGFloat {
    func toRadians() -> CGFloat {
        return self * CGFloat(Double.pi) / 180.0
    }
}

I'm using two shape layers, one for the circle in the left side and one for the arcShape in the right side.

My result using the above snippet:

enter image description here


Solution

  • Using some trigonometry you can get the exact result you need no matter what size the view's frame is. The following code is based on your original code with some changed calculations. It also allows for a user define line width and adjust itself so the stroked line is all within the confines of the view.

    class ArcView : UIView {
        var lineWidth = 1.5
    
        override init(frame: CGRect) {
            super.init(frame: frame)
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
        }
    
        func setupViewWith(textToDisplay:String, imageNamed:String, backgroundColor:UIColor) {
            setupView(backgroundColor: backgroundColor)
        }
    
        func setupView(backgroundColor:UIColor) {
            let criclepath = getCirclePath()
            let shapeLayer = CAShapeLayer()
            shapeLayer.position = .zero
            shapeLayer.path = criclepath.cgPath
            shapeLayer.lineWidth = lineWidth
            shapeLayer.strokeColor = UIColor.white.cgColor
            shapeLayer.fillColor = UIColor.white.cgColor
            self.layer.addSublayer(shapeLayer)
    
            let supportpath = getPath()
            let supportLayer = CAShapeLayer()
            supportLayer.path = supportpath.cgPath
            supportLayer.lineWidth = lineWidth
            supportLayer.strokeColor = UIColor.white.cgColor
            supportLayer.fillColor = backgroundColor.cgColor
            self.layer.addSublayer(supportLayer)
        }
    
        func getPath() -> UIBezierPath {
            let height = self.frame.size.height - lineWidth
            let width = self.frame.size.width - lineWidth
            let halfHeight = height / 2
            // Adjust the ratio here based on how much of a gap you want. 15% seems to look OK for both small and large views
            let radius = halfHeight * 1.15
            // The next two lines calculate the end points of the concave curve on the left end
            let xOffset = sqrt(radius * radius - halfHeight * halfHeight)
            let angle = atan2(halfHeight, xOffset)
    
            let path = UIBezierPath()
            // Start at top-left end point
            path.move(to: CGPoint(x: halfHeight + xOffset + lineWidth / 2, y: lineWidth / 2))
            // Draw top line
            path.addLine(to: CGPoint(x: width - halfHeight + lineWidth / 2, y: lineWidth / 2))
            // Draw right-end convex curve
            path.addArc(withCenter: CGPoint(x: width - halfHeight, y: halfHeight + lineWidth / 2),
                        radius: halfHeight,
                        startAngle: CGFloat(270).toRadians(),
                        endAngle: CGFloat(90).toRadians(),
                        clockwise: true)
            // Draw bottom line
            path.addLine(to: CGPoint(x: halfHeight + xOffset + lineWidth / 2, y: height + lineWidth / 2))
            // Draw left-end concave curve
            path.addArc(withCenter: CGPoint(x: halfHeight + lineWidth / 2, y: halfHeight + lineWidth / 2),
                        radius: radius,
                        startAngle: angle,
                        endAngle: -angle,
                        clockwise: false)
    
            path.close()
    
            return path
        }
    
        func getCirclePath() -> UIBezierPath {
            let path = UIBezierPath(ovalIn: CGRect(x: lineWidth / 2,
                                                   y: lineWidth / 2,
                                                   width: self.frame.size.height - lineWidth,
                                                   height: self.frame.size.height - lineWidth))
            return path
        }
    }
    
    extension CGFloat {
        func toRadians() -> CGFloat {
            return self * CGFloat(Double.pi) / 180.0
        }
    }
    

    Here's some example code. The above and the following can all be copied into a playground to test the results.

    let v = ArcView(frame: CGRect(x: 0, y: 0, width: 400, height: 120))
    v.lineWidth = 4
    v.backgroundColor = .black
    v.setupView(backgroundColor: .blue)
    

    Here's the output:

    enter image description here