SwiftUI AsyncImage with .fill ContentMode Covers Buttons in ZStack iOS-17

185 views Asked by At

I am working on a SwiftUI view that displays an image fetched from a URL using AsyncImage. I want the image to fill the entire screen, and I have a row of buttons overlaid at the bottom of the image within a ZStack. However, when I set the AsyncImage's contentMode to .fill, the buttons are not visible. Changing the contentMode to .fit makes the buttons appear, but the image no longer fills the entire screen as desired.

Here is the code snippet demonstrating the issue:

import SwiftUI

struct ImageViewTest: View {
    private let wallpaperPath = "https://w.wallhaven.cc/full/7p/wallhaven-7pmj9o.jpg"
    
    var body: some View {
        ZStack(alignment: .center) {
            AsyncImage(url: URL(string: wallpaperPath)) { phase in
                switch phase {
                case .empty:
                    ProgressView()
                case .failure:
                    Text("Failed to fetch image")
                case .success(let image):
                    image
                        .resizable()
                        .aspectRatio(contentMode: .fill) // Changing this to .fit shows the buttons
                        .clipped()
                @unknown default:
                    fatalError()
                }
            }
            .ignoresSafeArea(.all)
            
            VStack {
                Spacer()
                HStack {
                    Button(action: {}) {
                        Image(systemName: "square.and.arrow.down")
                            .foregroundStyle(.blue)
                    }
                    Spacer()
                    Button(action: {}) {
                        Image(systemName: "heart")
                            .foregroundStyle(.blue)
                    }
                    Spacer()
                    Button(action: {}) {
                        Image(systemName: "arrow.down.circle")
                            .foregroundStyle(.blue)
                    }
                }
                .padding()
                .background(.ultraThinMaterial)
                .cornerRadius(10)
            }
        }
    }
}

#Preview {
    ImageViewTest()
}


My goal is to have the image fill the entire screen while keeping the buttons visible at the bottom. I would appreciate any suggestions or insights into what might be causing the buttons to be covered when using .fill for the contentMode.

  • Changing the contentMode from .fill to .fit, which shows the buttons but does not meet the requirement of the image filling the screen.
2

There are 2 answers

0
Mojtaba Hosseini On BEST ANSWER

That's because the image caused the ZStack's width to overflow the screen and you need to prevent this somehow.

iOS 17

If you are targeting iOS 17 or later, adding the following modifier to the AsyncImage would be enough:

.containerRelativeFrame(.horizontal)

iOS 13 or later

One way is to use background modifier on the VStack instead of using ZStack like:

VStack { ,,, }
    .background {
        AsyncImage(url: URL(string: wallpaperPath)) { phase in ,,, }
        ,,,
    }

Both of the above methods would cause the view appear like this:

Demo


0
Sweeper On

The reason this happens is that the image has a large width, making the ZStack, and by extension the HStack at the bottom, horizontally too. This makes the two buttons on the sides move outside of the bounds of the screen.

To fix this, you should limit the image's size to the screen size. You can do this by reading the available space using a GeometryReader:

GeometryReader { geo in
    image
        .resizable()
        .aspectRatio(contentMode: .fill)
        // If the image can also be very large vertically, 
        // it will push the `HStack` downwards.
        // so I recommend constraining the heigh as well
        .frame(maxWidth: geo.size.width, maxHeight: geo.size.height)
}

Alternatively, add .containerRelativeFrame([.horizontal, .vertical]) on the AsyncImage on iOS 17+.

enter image description here

IMO, it still isn't very visible because the contrast is too low. I recommend changing the tint colors of the buttons.