Reverse masking in SwiftUI using blend modes


Greetings, traveler!

SwiftUI gives us mask(_:), which works well when you want to reveal part of a view.

What it does not give us is the inverse version — a way to cut shapes out of a view and let whatever sits behind it show through.

Let’s walk through a clean and practical way to build a reversed mask in SwiftUI and look at why it works.

The effect we want

Imagine a colored card where a shape is carved out of the surface.

The card remains visible everywhere except where the shape sits.
That area becomes transparent.

In UIKit you might reach for Core Graphics or layer masks.
In SwiftUI the solution lives in blend modes.

A simple reversed mask modifier

Here’s the modifier I use:

extension View {
    func reversedMask<M: View>(
        alignment: Alignment = .center,
        @ViewBuilder mask: () -> M
    ) -> some View {
        overlay(alignment: alignment) {
            mask().blendMode(.destinationOut)
        }
        .compositingGroup()
    }
}

It’s short, but there’s a lot happening under the hood.

Using it in a real view

In my demo grid each cell is a rounded rectangle with a symbol cut out:

RoundedRectangle(cornerRadius: 30)
    .fill(color)
    .reversedMask {
        Image(systemName: "heart.fill")
            .resizable()
            .scaledToFit()
            .padding()
    }

When the symbol changes, a new shape gets carved out of the card.
The background gradient becomes visible through the hole.

The same modifier becomes even more interesting once you start stacking multiple surfaces.

Here’s a banner-style layout built with ZStack:

ZStack {
    RoundedRectangle(cornerRadius: 30)
        .fill(.ultraThinMaterial)
    
    RoundedRectangle(cornerRadius: 30)
        .fill(.purple)
        .reversedMask {
            RoundedRectangle(cornerRadius: 30)
                .blur(radius: 30)
        }
}
.frame(maxWidth: .infinity)
.frame(height: 200)

The bottom layer uses ultraThinMaterial, which gives a translucent, glass-like surface. On top of it sits a solid blue rounded rectangle. That blue layer then cuts out a blurred version of the same rounded shape.

Because the mask is blurred, the edges of the cut-out are soft. Instead of a hard hole, the material underneath gently fades through.

This pattern works well for banners, headers, floating cards, and interactive controls where you want the background to subtly show through the surface.

Why this works

To understand this, it helps to know how blend modes think about drawing.

When SwiftUI renders a view hierarchy, each layer is drawn in order:

• the existing content is called the destination
• the overlay you draw on top is called the source

BlendMode.destinationOut keeps the destination only where the source is transparent.

A mode that you use to erase any of the background that is covered by opaque source pixels.

Where the source is opaque, the destination gets removed.

In practice:

• your rounded rectangle is the destination
• the symbol is the source
• the symbol punches a hole through the rectangle

That’s the entire trick.

Why compositingGroup() is necessary

Without compositingGroup(), SwiftUI often applies the blend mode per layer instead of as a unified surface.
The result ranges from nothing happening to partially broken transparency.

compositingGroup() forces SwiftUI to:

  1. Render the view and its overlay into a single offscreen buffer
  2. Apply the blend mode inside that buffer

Once everything lives in the same compositing context, the cut-out behaves consistently.

Conclusion

SwiftUI’s rendering pipeline looks high-level, but once you start leaning into blend modes and composition, it becomes surprisingly expressive.

This reversed mask is a good example of that.

If you’re already building modern SwiftUI interfaces with materials and depth, this is a technique worth keeping in your toolbox.