-1

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

HangarRash
  • 7,314
  • 5
  • 5
  • 32
  • 2
    It's unclear what issue you are having. The second picture shows the concave curve. What do you need help with? – HangarRash Jun 14 '23 at 04:50
  • I can't able to find the exact control points for the concave curve..I'm just harded the control point , help me find out the exact control points to draw perfect concave curve that shows in picture 1 – Karthi Rasu Jun 14 '23 at 05:17
  • You should use `addArcWithCenter:radius:startAngle:endAngle:clockwise:` instead of use the curve with control points. – Larme Jun 14 '23 at 05:26
  • Please do not destroy your question after getting answers. It would not have been answerable without the two pictures and your code. – HangarRash Jun 14 '23 at 14:43

2 Answers2

2

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

HangarRash
  • 7,314
  • 5
  • 5
  • 32
0

The result is close to the depicted design. The left circle may need to move to the left to compensate for the shape's stroke width which extend beyond the UIBezierPath initializer's dimensions, causing the squished appearance between the circle and the enclave.

For example (and not correct or tested as commented below, just an illustration):

shapeLayer.position = CGPoint(x: -strokeOffset, y: 0)

Additionally for the circle on the left, change backgroundColor here to the fill color depicted in the spec: shapeLayer.fillColor = backgroundColor.cgColor

Morkrom
  • 40
  • 5
  • The OP's code hardcodes several values that only work (mostly) for one specific height. This little change doesn't actually resolve most of the issues. And moving the circle to the left will actually add more problems. – HangarRash Jun 14 '23 at 06:17
  • I should have plugged it in and posted a snippet to be correct yes. I figured it was enough info to lead the asker to consider the stroke width in their own code. – Morkrom Jun 14 '23 at 22:27