Search code examples
imageswiftuipaddingclipvstack

SwiftUI Image Clip to ZStack - Image extends to edges of screen ignoring padding of parent stack


This is my first question on here.

I want padding at the edges of this View:

struct Test3: View {
    var body: some View {
        VStack {
            ZStack {
                Color.blue
                    Image("SplashPage")
                        .resizable()
                        .scaledToFill()
                }
            ZStack {
                Color.gray
                Button("touch me", action: {})
            }
           .frame(width: .infinity, height: 48)
        }
        .padding(24)
    }
}

I've searched here and thought this post would solve my problems:

SwiftUI Image clipsToBounds

.clipped() -> Nope.

I've searched online. Asked ChatGPT. Looked at hackingwithswift. Looking for "mask image SwiftUI" and "SwiftUI clip image to ZStack"

... nope.

I've been trying to nail down what I'm doing wrong and I just can't figure it out:

Screenshot

Screenshot

I have a VStack which contains 2 ZStacks. This VStack has padding of 24 all around.

In the first VStack, I added an image that is .resizable() and .scaledToFill()

I expected the image to be clipped respecting the 24 point padding. I expect this to respect the padding on an iPhone SE and an iPhone 14 Pro or whatever large emulator.

The second VStack has a button with .frame(width: .infinity, height: 48). If I comment this line out, then the image behaves as expected!


Solution

  • This problem is being caused by the scaledToFill() on the Image. It seems to fill the full screen width and ignores the padding at the sides.

    You found it was working when you removed the .frame modifier from the second ZStack. I am not sure why this helped, but there was a separate issue here too. Setting a width of .infinity is invalid (and I was seeing errors in the console). It needs to be maxWidth instead.

    So here are a couple of workarounds for the image scaling:

    1. Surround the Image with a GeometryReader

    A GeometryReader can be used to measure the space that is really available and this size can be set on the Image. After setting the size, it also needs to be .clipped().

    VStack {
        ZStack {
            Color.blue
            GeometryReader { proxy in
                Image(systemName: "ladybug.fill") //  "SplashPage"
                    .resizable()
                    .scaledToFill()
                    .foregroundStyle(.orange)
                    .frame(width: proxy.size.width, height: proxy.size.height)
                    .clipped()
            }
        }
        ZStack {
            Color.gray
            Button("touch me", action: {})
        }
        .frame(maxWidth: .infinity)
        .frame(height: 48)
    }
    .padding(24)
    

    2. Show the Image as an overlay

    The Color that you are using in the background will expand to fill all of the space available. If the Image is applied as an overlay to this Color then it will adopt the same frame size. The surrounding ZStack is then redundant and can be omitted.

    With this approach, the .clipped() modifier needs to be applied after the overlay to the Color, not to the Image.

    VStack {
        Color.blue
            .overlay {
                Image(systemName: "ladybug.fill") //  "SplashPage"
                    .resizable()
                    .scaledToFill()
                    .foregroundStyle(.orange)
            }
            .clipped()
        ZStack {
            Color.gray
            Button("touch me", action: {})
        }
        .frame(maxWidth: .infinity)
        .frame(height: 48)
    }
    .padding(24)
    

    The result is the same in both cases:

    Screenshot