Cutout Shape in SwiftUI
Recently I was tasked with writing a barcode scanner screen at my day job. The specific design had a rounded rectangle clipping on the transparent overaly. That seems simple until I realized that the rounded rect was cutout not filled. Turns out the solution is relatively straightforward: use even-odd fill.
Imagine two shapes, a rectangle that represents the full size overlay that would cover the entire screen, and a rounded rectangle that is centered on the rectangle. All I thought I needed to do was use the clipShape
API, but there is more to it than that.
Color.blue.opacity(0.5)
.cutout(RoundedRectangle(cornerRadius: 16).scale(0.5))
This yields the following result:

A little bit of googling suggests using the even-odd fill mode. That too comes a little short without fully understanding how it works. The naive addition of just adding even-odd fill looks like this.
Color.blue.opacity(0.5)
.clipShape(RoundedRectangle(cornerRadius: 16).scale(0.5), style: FillStyle(eoFill: true))

After studying the examples more closely, I realized the problem: even-odd fill only affects paths with strokes that overlap. My example doesn't have any overlap so there is no difference. The solution then became straightforward; add a rectangle before adding my rounded rect so the rounded rect will all be overlap and thus used for the clipping.
struct CutoutRoundedRectangle: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.addPath(Rectangle().path(in: rect))
path.addPath(RoundedRectangle(cornerRadius: 16).scale(0.5).path(in: rect))
}
}
}
Now this shape cuts out the area I need:
Color.blue.opacity(0.5)
.clipShape(CutoutRoundedRectangle(), style: FillStyle(eoFill: true))

That solution works just fine, but I really enjoy generalizing these kind of solutions so that the code is clear and potentially reusable in the future.
CutoutRoundedRectangle
can be nicely generalized to accept both shapes for arbirary shape stacking.
extension View {
func cutout<S: Shape>(_ shape: S) -> some View {
self.clipShape(StackedShape(bottom: Rectangle(), top: shape), style: FillStyle(eoFill: true))
}
}
struct StackedShape<Bottom: Shape, Top: Shape>: Shape {
var bottom: Bottom
var top: Top
func path(in rect: CGRect) -> Path {
return Path { path in
path.addPath(bottom.path(in: rect))
path.addPath(top.path(in: rect))
}
}
}
Improving the call site to this:
Color.blue.opacity(0.5)
.cutout(RoundedRectangle(cornerRadius: 16).scale(0.5))