iosswiftswiftui

How to darken a background image in SwiftUI but exclude the area inside a transparent mockup?


Cover image

Mockup image

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!


Solution

  • 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.

    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()
    }
    

    Screenshot