Search code examples
iosswiftpathuibezierpathbezier

How to draw rounded rect that wraps around circular button?


I want to design the highlighted portion of curve of the below screen snapshot using bezier path in swift.

enter image description here


Solution

  • It’s just a little trigonometry to figure out the arcs around that button in the bottom right hand corner, e.g.

    func updatePath() {
        let buttonCenter = CGPoint(x: bounds.maxX - circleRadius, y: bounds.maxY - circleRadius)
    
        circleShapeLayer.path = UIBezierPath(arcCenter: buttonCenter, radius: circleRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath // red
    
        let angle1 = acos((circleRadius - cornerRadius) / (circleRadius + spaceRadius + cornerRadius))
        let angle2 = acos((circleRadius - bottomDistance - cornerRadius) / (circleRadius + spaceRadius + cornerRadius))
    
        let arc1Center = CGPoint(x: bounds.maxX - cornerRadius,
                                 y: buttonCenter.y - (circleRadius + cornerRadius + spaceRadius) * sin(angle1))
    
        let path = UIBezierPath()
    
        path.move(to: CGPoint(x: bounds.maxX, y: bounds.minY + cornerRadius))
        path.addArc(withCenter: arc1Center, radius: cornerRadius, startAngle: 0, endAngle: .pi / 2 + (.pi / 2 - angle1), clockwise: true) // blue
        path.addArc(withCenter: buttonCenter, radius: circleRadius + spaceRadius, startAngle: 2 * .pi - angle1, endAngle: .pi / 2 + angle2, clockwise: false) // green
    
        let arc2Center = CGPoint(x: buttonCenter.x - (circleRadius + cornerRadius + spaceRadius) * sin(angle2), y: bounds.maxY - bottomDistance - cornerRadius)
    
        path.addArc(withCenter: arc2Center, radius: cornerRadius, startAngle: -(.pi / 2 - angle2), endAngle: .pi / 2, clockwise: true) // yellow
        path.addArc(withCenter: CGPoint(x: bounds.minX + cornerRadius, y: bounds.maxY - (bottomDistance + cornerRadius)), radius: cornerRadius, startAngle: .pi / 2, endAngle: .pi, clockwise: true) // cyan
        path.addArc(withCenter: CGPoint(x: bounds.minX + cornerRadius, y: bounds.minY + cornerRadius), radius: cornerRadius, startAngle: .pi, endAngle: .pi * 3 / 2, clockwise: true) // white
        path.addArc(withCenter: CGPoint(x: bounds.maxX - cornerRadius, y: bounds.minY + cornerRadius), radius: cornerRadius, startAngle: .pi * 3 / 2, endAngle: 2 * .pi, clockwise: true) // black
        path.close()
    
        backgroundShapeLayer.path = path.cgPath
    }
    

    Yielding:

    enter image description here

    Or, if you want to match the above strokes, here it is color coded to the comments in the code above:

    enter image description here


    For example:

    @IBDesignable
    class BackgroundView: UIView {
        @IBInspectable var cornerRadius:   CGFloat = 10 { didSet { setNeedsLayout() } }
        @IBInspectable var spaceRadius:    CGFloat = 10 { didSet { setNeedsLayout() } }
        @IBInspectable var circleRadius:   CGFloat = 50 { didSet { setNeedsLayout() } }
        @IBInspectable var bottomDistance: CGFloat = 30 { didSet { setNeedsLayout() } }
    
        private let backgroundShapeLayer: CAShapeLayer = {
            let shapeLayer = CAShapeLayer()
            shapeLayer.fillColor = UIColor.white.cgColor
            shapeLayer.strokeColor = UIColor.clear.cgColor
            return shapeLayer
        }()
    
        private let circleShapeLayer: CAShapeLayer = {
            let shapeLayer = CAShapeLayer()
            shapeLayer.fillColor = UIColor.white.cgColor
            shapeLayer.strokeColor = UIColor.clear.cgColor
            return shapeLayer
        }()
    
        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()
        }
    }
    
    private extension BackgroundView {
    
        func configure() {
            layer.addSublayer(circleShapeLayer)
            layer.addSublayer(backgroundShapeLayer)
        }
    
        func updatePath() {
            let buttonCenter = CGPoint(x: bounds.maxX - circleRadius, y: bounds.maxY - circleRadius)
    
            circleShapeLayer.path = UIBezierPath(arcCenter: buttonCenter, radius: circleRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
    
            let angle1 = acos((circleRadius - cornerRadius) / (circleRadius + spaceRadius + cornerRadius))
            let angle2 = acos((circleRadius - bottomDistance - cornerRadius) / (circleRadius + spaceRadius + cornerRadius))
    
            let arc1Center = CGPoint(x: bounds.maxX - cornerRadius,
                                     y: buttonCenter.y - (circleRadius + cornerRadius + spaceRadius) * sin(angle1))
    
            let path = UIBezierPath()
    
            path.move(to: CGPoint(x: bounds.maxX, y: bounds.minY + cornerRadius))
            path.addArc(withCenter: arc1Center, radius: cornerRadius, startAngle: 0, endAngle: .pi / 2 + (.pi / 2 - angle1), clockwise: true)
            path.addArc(withCenter: buttonCenter, radius: circleRadius + spaceRadius, startAngle: 2 * .pi - angle1, endAngle: .pi / 2 + angle2, clockwise: false)
    
            let arc2Center = CGPoint(x: buttonCenter.x - (circleRadius + cornerRadius + spaceRadius) * sin(angle2), y: bounds.maxY - bottomDistance - cornerRadius)
    
            path.addArc(withCenter: arc2Center, radius: cornerRadius, startAngle: -(.pi / 2 - angle2), endAngle: .pi / 2, clockwise: true)
            path.addArc(withCenter: CGPoint(x: bounds.minX + cornerRadius, y: bounds.maxY - (bottomDistance + cornerRadius)), radius: cornerRadius, startAngle: .pi / 2, endAngle: .pi, clockwise: true)
            path.addArc(withCenter: CGPoint(x: bounds.minX + cornerRadius, y: bounds.minY + cornerRadius), radius: cornerRadius, startAngle: .pi, endAngle: .pi * 3 / 2, clockwise: true)
            path.addArc(withCenter: CGPoint(x: bounds.maxX - cornerRadius, y: bounds.minY + cornerRadius), radius: cornerRadius, startAngle: .pi * 3 / 2, endAngle: 2 * .pi, clockwise: true)
            path.close()
    
            backgroundShapeLayer.path = path.cgPath
        }
    }