Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a custom Shape in SwiftUI with rounded top corners and a jagged bottom edge resembling torn paper?

I am working with SwiftUI and need to implement a custom Shape that mimics a specific design. The shape is a rectangle but with distinct features: the top corners are rounded, and the bottom edge resembles a row of rounded triangles, giving it the appearance of torn paper.

reference

Could someone guide me on how to create this specific shape or suggest a better approach to achieve the torn paper effect on the bottom edge of a rectangle in SwiftUI?

Here's what I've tried so far:

struct CustomShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()

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

        let bottomLeft = CGPoint(x: 0, y: height)
        let bottomRight = CGPoint(x: width, y: height)
        let topLeft = CGPoint(x: 0, y: 0)
        let topRight = CGPoint(x: width, y: 0)

        path.move(to: bottomRight)

        path.addLine(to: CGPoint(x: bottomRight.x, y: topRight.y))

        path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y))
        
        path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y))
        
        let triangleWidth: CGFloat = 11
        let triangleHeight: CGFloat = 6

        var x: CGFloat = 0
        while x <= rect.width {
            let startX = x
            let endX = x + triangleWidth
            let midX = (startX + endX) / 2

            path.move(to: CGPoint(x: startX, y: rect.maxY))
            path.addLine(to: CGPoint(x: midX, y: rect.maxY - triangleHeight))
            path.addLine(to: CGPoint(x: endX, y: rect.maxY))

            x += triangleWidth
        }

        return path
    }
}

poop

like image 489
Danila Belyi Avatar asked Oct 27 '25 20:10

Danila Belyi


1 Answers

I will parameterise this shape by the radius of the top corners, the corner radius of the triangles, and the size of the triangles.

The given rect might not fit an integer number of these triangles. It is not clear how the shape should look in that case, so I will treat the width of the triangle as a minimum width. The actual triangles might be wider than that, in order to fit an integer number of triangles in the given rect.

Here is the code. The main idea is to use addArc(tangent1End:tangent2End:radius:) so that every line we draw has a rounded corner. See also the explanation in the comments.

struct CustomShape: Shape {
    let topCornersRadius: CGFloat
    let trianglesCornerRadius: CGFloat
    let triangleHeight: CGFloat
    let minTriangleWidth: CGFloat
    
    func path(in rect: CGRect) -> Path {
        Path { path in
            // calculate the minimum width of the triangles,
            // such that it is at least minTriangleWidth, and
            // rect.width fits an integer number of such triangles
            let triangleCount = Int(rect.width / minTriangleWidth)
            let actualTriangleWidth = rect.width / CGFloat(triangleCount)
            
            // start from the top left
            path.move(to: .init(x: rect.minX + topCornersRadius, y: rect.minY))
            // draw the rounded corner for the top left corner
            path.addArc(
                tangent1End: rect.origin,
                tangent2End: .init(x: rect.minX, y: rect.minX + topCornersRadius),
                radius: topCornersRadius
            )
            
            // draw the left side of the shape
            var tangent1End = CGPoint(x: rect.minX, y: rect.maxY)
            var tangent2End = tangent1End.applying(.init(translationX: actualTriangleWidth / 2, y: -triangleHeight))
            path.addArc(tangent1End: tangent1End, tangent2End: tangent2End, radius: trianglesCornerRadius / 2)
            
            // draw most of the triangles
            // each iteration draws one side of a triangle
            // we don't draw the right side of the last triangle in the loop,
            // because it will have a different tangent2End
            for i in 0..<(2 * triangleCount - 1) {
                tangent1End = tangent2End
                tangent2End = tangent1End.applying(.init(
                    translationX: actualTriangleWidth / 2,
                    y: triangleHeight * (i.isMultiple(of: 2) ? 1 : -1)
                ))
                path.addArc(tangent1End: tangent1End, tangent2End: tangent2End, radius: trianglesCornerRadius)
            }
            
            // draw the right side of the last triangle
            path.addArc(tangent1End: tangent2End, tangent2End: .init(x: rect.maxX, y: rect.minY), radius: trianglesCornerRadius / 2)
            
            // draw the right side of the shape
            path.addArc(
                tangent1End: .init(x: rect.maxX, y: rect.minY),
                tangent2End: rect.origin,
                radius: topCornersRadius
            )
            
            // connects the top right corner of the shape to the top left corner
            path.closeSubpath()
        }
    }
}

Example output:

enter image description here

like image 147
Sweeper Avatar answered Oct 29 '25 10:10

Sweeper



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!