Skip to content

The Power of Overlays (SwiftUI)

A SwiftUI guide for coding agents. Also covers overlay modifier tricks, structural identity, avoid if branching viewbuilder, hero animation without navigationstack, matchedgeometryeffect overlay, view loses state on toggle.

An overlay draws one view on top of another. Beyond decoration, it solves two real problems: keeping a view's structural identity, and building hero-style transitions that NavigationStack cannot.

Keep Structural Identity

An if/else in a ViewBuilder creates two different views. Each toggle destroys one branch and builds the other, so a custom button loses its state every time.

// Loses state: SwiftUI rebuilds the button on every toggle.
if isDownloading {
    ProgressView()
} else {
    DownloadButton("Download") { /* ... */ }
}

Keep one view alive and layer the conditional part in an overlay instead. disabled is an inert modifier, so the button's identity survives.

DownloadButton { /* start download */ }
    .disabled(isDownloading)
    .overlay {
        if isDownloading {
            ProgressView()
        }
    }

Custom Transitions Without Navigation

NavigationStack transitions cannot be customized, and matchedGeometryEffect does not work across a push. An overlay driven by selection state gives you the hero animation instead.

@State private var selectedImage: String?
@Namespace private var hero

LazyVGrid(columns: Array(repeating: .init(.flexible()), count: 3)) {
    ForEach(images, id: \.self) { image in
        Image(systemName: selectedImage == image ? "" : image)
            .resizable()
            .scaledToFit()
            .matchedGeometryEffect(id: image, in: hero)
            .onTapGesture { selectedImage = image }
    }
}
.overlay {
    if let image = selectedImage {
        Image(systemName: image)
            .resizable()
            .scaledToFill()
            .matchedGeometryEffect(id: image, in: hero)
            .onTapGesture { selectedImage = nil }
    }
}
.animation(.default, value: selectedImage)

The grid cell and the overlay share one geometry id, so selecting a cell morphs it into the full-size view. You own the navigation state yourself, which is the price of the effect.

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-the-power-of-overlays" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems
The Power of Overlays (SwiftUI): SwiftUI guide for coding agents | Better Design