Search code examples
iosswiftcalayeruibezierpath

Trim UIView with 2 arcs


I have a UIView and I want to trim it with two circles, like I've drawn(sorry for the quality).

My code:

final class TrimmedView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)

        let size = CGSize(width: 70, height: 70)
        let innerRadius: CGFloat = 366.53658283002471
        let innerBottomRadius: CGFloat = 297.88543112651564

        let path = UIBezierPath()
        path.move(to: CGPoint(x: -innerRadius + (size.width / 2), y: innerRadius))
        path.addArc(withCenter: CGPoint(x: size.width / 2, y: innerRadius), radius: innerRadius, startAngle: CGFloat.pi, endAngle: 0, clockwise: true)
        path.move(to: CGPoint(x: -innerBottomRadius + (size.width / 2), y: innerBottomRadius))
        path.addArc(withCenter: CGPoint(x: size.width / 2, y: innerBottomRadius), radius: innerBottomRadius, startAngle: 0, endAngle: CGFloat.pi, clockwise: true)

        path.close()
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = path.cgPath
        shapeLayer.shadowPath = path.cgPath
        layer.mask = shapeLayer
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
}

ViewController:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    let view = UIView(frame: CGRect(origin: CGPoint(x: (self.view.bounds.width - 70) / 2, y: (self.view.bounds.height - 70) / 2), size: CGSize(width: 70, height: 70)))
    view.backgroundColor = .red
    self.view.addSubview(view)
    let view1 = TrimmedView(frame: view.frame)
    view1.backgroundColor = .yellow
    self.view.addSubview(view1)
}

I got this result. It seems for me that top trimming works but the bottom doesn't and I don't know why. Any help would be appreciated. Thanks.


Solution

  • Here is a custom view that should give you what you want.

    The UIBezierPath uses QuadCurves for the top "convex" arc and the bottom "concave" arc.

    It is marked @IBDesignable so you can see it at design-time in IB / Storyboard. The "height" of the arc and the fill color are each set as @IBInspectable so you can adjust those values at design-time as well.

    To use it in Storyboard:

    • Add a normal UIView
    • change the Class to BohdanShapeView
    • in the Attributes Inspector pane, set the Arc Offset and the Fill Color
    • set the background color as with a normal view (you'll probably use clear)

    Result:

    enter image description here

    enter image description here

    To use it via code:

    let view1 = BohdanShapeView(frame: view.frame)
    view1.fillColor = .systemTeal
    view1.arcOffset = 10
    self.view.addSubview(view1)
    

    Here is the class:

    @IBDesignable
    class BohdanShapeView: UIView {
    
        @IBInspectable var arcOffset: CGFloat = 0.0
        @IBInspectable var fillColor: UIColor = UIColor.white
    
        let shapeLayer = CAShapeLayer()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
    
        func commonInit() -> Void {
            // add the shape layer
            layer.addSublayer(shapeLayer)
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            // fill color for the shape
            shapeLayer.fillColor = self.fillColor.cgColor
    
            let width = bounds.size.width
            let height = bounds.size.height
    
            let bezierPath = UIBezierPath()
    
            // start at arcOffset below top-left
            bezierPath.move(to: CGPoint(x: 0.0, y: 0.0 + arcOffset))
    
            // add curve to arcOffset below top-right
            bezierPath.addQuadCurve(to: CGPoint(x: width, y: 0.0 + arcOffset), controlPoint: CGPoint(x: width * 0.5, y: 0.0 - arcOffset))
    
            // add line to bottom-right
            bezierPath.addLine(to: CGPoint(x: width, y: height))
    
            // add curve to bottom-left
            bezierPath.addQuadCurve(to: CGPoint(x: 0.0, y: height), controlPoint: CGPoint(x: width * 0.5, y: height - arcOffset * 2.0))
    
            // close the path
            bezierPath.close()
    
            shapeLayer.path = bezierPath.cgPath
    
        }
    
    }