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.
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
}
}
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: