Search code examples
swiftuipathdrawingshapes

SwiftUI Custom Rounded RectangleShape


Does anyone know how to make a shape like this? enter image description here

I followed apples tutorial and came up with this but it seems overly complicated for what I want to do and not perfectly rounded. It also seems hard to implement and maintain to me. Seems like there is a simpler way. Thank you for your thoughts.

struct HexagonParameters {
    
    struct Segment {
        let line: CGPoint
        let curve: CGPoint
        let control: CGPoint
    }
    
    static let adjustment: CGFloat = 0.085
    
    static let topSholdHt = 0.13
    static let botSholdHt = 0.87
    
    static let segments = [
        
        Segment( // top center
                line:    CGPoint(x: 0.80, y: topSholdHt), // top right
                curve:   CGPoint(x: 0.20, y: topSholdHt), // top left
                control: CGPoint(x: 0.50, y: 0.00) // center / peak
            ),
            Segment( // upper left
                line:    CGPoint(x: 0.05, y: 0.20),
                curve:   CGPoint(x: 0.00, y: 0.30),
                control: CGPoint(x: 0.0, y: 0.23)
            ),
            Segment( // lower left
                line:    CGPoint(x: 0.00, y: 0.70),
                curve:   CGPoint(x: 0.05, y: 0.80),
                control: CGPoint(x: 0.00, y: 0.78)
            ),
            Segment( // bottom center
                line:    CGPoint(x: 0.20, y: botSholdHt),
                curve:   CGPoint(x: 0.80, y: botSholdHt),
                control: CGPoint(x: 0.50, y: 1.00)
            ),
            Segment(    // lower right
                line:    CGPoint(x: 0.95, y: 0.80),
                curve:   CGPoint(x: 1.00, y: 0.70),
                control: CGPoint(x: 1.00, y: 0.78)
            ),
            Segment(
                line:    CGPoint(x: 1.00, y: 0.30),
                curve:   CGPoint(x: 0.95, y: 0.20),
                control: CGPoint(x: 1.00, y: 0.23)
            )
    ]
    
}

The struct is used like this

struct ContentView: View {
    var body: some View {
        Path { path in
            var width: CGFloat = 300.0
            let height = width
            path.move(
                to: CGPoint(
                    x: width * 0.95,
                    y: height * 0.20
                )
            )
            
            HexagonParameters.segments.forEach { segment in
                path.addLine(
                    to: CGPoint(
                        x: width * segment.line.x,
                        y: height * segment.line.y
                    )
                )
                
                path.addQuadCurve(
                    to: CGPoint(
                        x: width * segment.curve.x,
                        y: height * segment.curve.y),
                    control: CGPoint(
                        x: width * segment.control.x,
                        y: height * segment.control.y
                    )
                )
            }
        }
        .stroke(style: StrokeStyle(lineWidth: 2))
        .foregroundColor(Color.black)
        .padding(.horizontal)

    }
}

Solution

  • As I said in my comment, it looks like you solved it yourself, your code produces the shape in the question.

    However, you remarked that your shape was:

    not perfectly rounded

    So I am wondering if you are trying to achieve a smoother shape which does not have the inverted arcs near the corners?

    Assuming this is the case, here is how it could be solved with a custom Shape. I wouldn't say it's any simpler to maintain than what you originally had, but at least it demonstrates a different approach.

    struct Lozenge: Shape {
        func path(in rect: CGRect) -> Path {
            let cornerRadius = min(rect.size.width, rect.size.height) * 0.1
            let middleHeight = rect.size.height * 0.15
            let cornerAngleRadians = atan2(rect.size.width, rect.size.height - (2 * middleHeight))
            let cornerAngleDegrees = cornerAngleRadians * 180 / Double.pi
    
            var path = Path()
            var x = rect.minX
            var y = rect.minY + cornerRadius + middleHeight
            path.move(to: CGPoint(x: x, y: y))
    
            // Top-left corner
            x += cornerRadius
            path.addArc(
                center: CGPoint(x: x, y: y),
                radius: cornerRadius,
                startAngle: .degrees(-180),
                endAngle: .degrees(-180 + cornerAngleDegrees),
                clockwise: false
            )
            // Middle bump on top
            x = rect.maxX - cornerRadius + (cornerRadius * cos(cornerAngleRadians))
            y = rect.minY + middleHeight + cornerRadius - (cornerRadius * sin(cornerAngleRadians))
            path.addQuadCurve(
                to: CGPoint(x: x, y: y),
                control: CGPoint(x: rect.midX, y: rect.minY - middleHeight)
            )
            // Top-right corner
            x = rect.maxX - cornerRadius
            y = rect.minY + cornerRadius + middleHeight
            path.addArc(
                center: CGPoint(x: x, y: y),
                radius: cornerRadius,
                startAngle: .degrees(-cornerAngleDegrees),
                endAngle: .degrees(0),
                clockwise: false
            )
            // Bottom-right corner
            y = rect.maxY - cornerRadius - middleHeight
            path.addArc(
                center: CGPoint(x: x, y: y),
                radius: cornerRadius,
                startAngle: .degrees(0),
                endAngle: .degrees(cornerAngleDegrees),
                clockwise: false
            )
            // Lower bump on bottom
            x = rect.minX + cornerRadius - (cornerRadius * cos(cornerAngleRadians))
            y = rect.maxY - middleHeight - cornerRadius + (cornerRadius * sin(cornerAngleRadians))
            path.addQuadCurve(
                to: CGPoint(x: x, y: y),
                control: CGPoint(x: rect.midX, y: rect.maxY + middleHeight)
            )
            // Bottom-left corner
            x = rect.minX + cornerRadius
            y = rect.maxY - cornerRadius - middleHeight
            path.addArc(
                center: CGPoint(x: x, y: y),
                radius: cornerRadius,
                startAngle: .degrees(180 - cornerAngleDegrees),
                endAngle: .degrees(180),
                clockwise: false
            )
            path.closeSubpath()
            return path
        }
    }
    
    struct ContentView: View {
        var body: some View {
            Lozenge()
                .stroke(lineWidth: 3)
                .frame(width: 300, height: 250)
                .background(.yellow)
        }
    }
    

    Lozenge