swiftuizstackswiftui-asyncimage

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


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.


Solution

  • 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