iosswiftuiviewuibezierpath

Combine two view into one shape UIKit in Swift


shape I am trying to make I am trying to make the above shape with UIKit. The small independent circle I am not concerned about, however, the semi-circle on the bottom left of the rounded rect is where I am getting stuck, especially given the border and the fact that the background color has a 70% alpha component (meaning it is partially transparent.)

I tried at first to just create two UIViews.
Directly using views resulted in: result of overlaying two views

Given that, I decided to try and override the UIView draw function. here is my attempt:

override func draw(_ rect: CGRect) {
    guard let context = UIGraphicsGetCurrentContext() else { return }

    let fillColor = configuration.isTransparent
    ? UIColor.primaryPurple.withAlphaComponent(0.7)
    : UIColor.primaryPurple

    let width = rect.width
    let height = rect.height

    let roundedRect = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: width, height: height - 8), cornerRadius: 24)
    let circle = UIBezierPath(ovalIn: CGRect(x: 24, y: height - 24, width: 24, height: 24))

    // Combine into one path
    let combinedPath = UIBezierPath()
    combinedPath.append(roundedRect)
    combinedPath.append(circle)
    combinedPath.usesEvenOddFillRule = false // Important: treat as single shape

    // Fill
    context.addPath(combinedPath.cgPath)
    context.setFillColor(fillColor.cgColor)
    context.fillPath()

    // Stroke
    context.addPath(combinedPath.cgPath)
    context.setStrokeColor(UIColor.primaryPurple.cgColor)
    context.setLineWidth(1)
    context.setLineJoin(.round)
    context.strokePath()
}

This resulted in:

draw function attempt

truthfully, I am not sure where to go from here


Solution

  • Another approach which you may find a little more flexible, we can use the .union method of CGPath:

    class AnotherCustomView: UIView {
        
        override func draw(_ rect: CGRect) {
            guard let context = UIGraphicsGetCurrentContext() else { return }
            
            let strokeColor: UIColor = .init(red: 0.367, green: 0.387, blue: 0.771, alpha: 1.0)
            let fillColor: UIColor = strokeColor.withAlphaComponent(0.7)
    
            // if we want to inset the drawing...
            //  we'll use 8 here to better show the shape
            //  for no inset, use 0
            var rectR: CGRect = rect.insetBy(dx: 8, dy: 8)
            
            let x = rectR.minX
            let y = rectR.minY
            
            let width = rectR.width
            let height = rectR.height - 8
            
            let roundedRect = CGPath(roundedRect: .init(x: x, y: y, width: width, height: height), cornerWidth: 24, cornerHeight: 24, transform: nil)
            let circle = CGPath(ellipseIn: .init(x: x + 24, y: y + height - 16, width: 24, height: 24), transform: nil)
            let combinedPath = roundedRect.union(circle)
            
            // Fill
            context.addPath(combinedPath)
            context.setFillColor(fillColor.cgColor)
            context.fillPath()
            
            // Stroke
            context.addPath(combinedPath)
            context.setStrokeColor(strokeColor.cgColor)
            context.setLineWidth(1)
            context.setLineJoin(.round)
            context.strokePath()
        }
        
    }
    

    With this example controller:

    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .systemBackground
    
            let v1 = AnotherCustomView(frame: .init(x: 40, y: 80, width: 160, height: 120))
            view.addSubview(v1)
            v1.backgroundColor = .black
            
        }
    
    }
    

    we get this output:

    result