I have a SwiftUI view where I display a background image with a mockup of an iPhone over it. The mockup has a transparent area to show the background image. I want to add a Color.black.opacity(0.2) overlay to darken the background image for better readability of the content. However, I want the dark overlay to exclude the transparent area of the mockup.
I tried the following approach:
Color.black.opacity(0.2)
.ignoresSafeArea()
.overlay(
Image(.mockup)
.resizable()
.scaledToFit()
.blendMode(.destinationOut)
)
This approach doesn’t seem to work as expected—the dark overlay is still visible over the mockup’s transparent area.
Here’s the full code for context:
import SwiftUI
struct ContentView: View {
@State var name: String = ""
var body: some View {
ZStack {
Image(.cover)
.resizable()
.scaledToFill()
.ignoresSafeArea()
VStack {
Image(.mockup)
Spacer()
Text("Lucas & Lavínia")
.font(.title)
.fontWeight(.bold)
Spacer().frame(height: 24)
TextField(
"",
text: $name,
prompt: Text("Nome").foreground(.white)
)
.padding(15)
.background(.white.opacity(0))
.overlay(
RoundedRectangle(
cornerRadius: 8
)
.stroke(
.white,
lineWidth: 1
)
)
.clipShape(.rect(cornerRadius: 8))
.foregroundStyle(.white)
Spacer().frame(height: 24)
Button {
} label: {
HStack {
Text("Selecionar fotos da galeria")
}
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.padding(.vertical, 15)
.background(.red)
.cornerRadius(40)
}
.frame(maxWidth: .infinity)
Spacer().frame(height: 24)
Button {
} label: {
Text("Capturar foto agora")
}
Spacer().frame(height: 35)
}
.frame(maxWidth: .infinity)
.foregroundStyle(.white)
.padding(.all)
.padding(.top, 25)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
}
#Preview {
ContentView()
}
How can I apply the black overlay only to the parts of the background image that are not covered by the mockup? Is there a way to mask or exclude the area inside the transparent section of the mockup? Or would I need a completely different approach to achieve this effect?
Any guidance or code examples would be greatly appreciated!
First of all, I would suggest showing the base image in the background of the ZStack
, instead of as the first layer of the ZStack
. This way, the overflow from scaling-to-fill will not cause the ZStack
to extend off-screen.
Then, a masking layer can be added as the first layer of the ZStack
, which is seen above the background image.
The mask is formed using semi-transparent Color.black
.
A RoundedRectangle
is overlayed over the semi-transparent background. The corner radius should approximately match the shape of the mockup.
The rounded rectangle is applied using .blendMode(.destinationOut)
. This causes the shape to be cut out from the underlying semi-transparent black layer.
The modifier .compositingGroup()
is applied, to prevent blend mode from burning deeper into lower layers.
To match the size and position of the cut-out with the mockup image in the VStack
, .matchedGeometryEffect
can be used. This requires a namespace:
@Namespace private var ns
Here is the example with updates applied:
ZStack {
Color.black
.opacity(0.5)
.ignoresSafeArea()
.overlay {
RoundedRectangle(cornerRadius: 40)
.inset(by: 4)
.fill(.black)
.matchedGeometryEffect(id: "mockup", in: ns, isSource: false)
.blendMode(.destinationOut)
}
.compositingGroup()
VStack {
Image(.mockup)
.resizable()
.scaledToFit()
.matchedGeometryEffect(id: "mockup", in: ns)
// ... other content as before
}
// ... modifiers as before
}
.background {
Image(.cover)
.resizable()
.scaledToFill()
.ignoresSafeArea()
}