SwiftUI Layout Issue - Bottom safe area being ignored for seemingly no reason.

Hello there, I ran into a very strange layout issue in SwiftUI. Here's the View:

struct OnboardingGreeting: View {
  /// ...
    let carouselSpacing: CGFloat = 7
    let carouselItemSize: CGFloat = 110
    let carouselVelocities: [CGFloat] = [0.5, -0.25, 0.3, 0.2]
    
    var body: some View {
        ZStack(alignment: Alignment(horizontal: .center, vertical: .iconToCarouselBottom)) {
            VStack(spacing: carouselSpacing) {
                ForEach(carouselVelocities, id: \.self) { velocityValue in
                    InfiniteHorizontalCarousel(
                        albumNames: albums,
                        artistNames: artists,
                        itemSize: carouselItemSize,
                        itemSpacing: carouselSpacing,
                        velocity: velocityValue
                    )
                }
            }
            .alignmentGuide(.iconToCarouselBottom) { context in
                context[VerticalAlignment.bottom]
            }
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
            
            LinearGradient(
                colors: [.black, .clear],
                startPoint: .bottom,
                endPoint: .top
            )
            
            // Top VStack
            VStack(spacing: 0) {
                RoundedRectangle(cornerRadius: 18)
                    .fill(.red)
                    .frame(width: 95, height: 95)
                    .alignmentGuide(.iconToCarouselBottom) { context in
                        context.height / 2
                    }
                Text("Welcome to Music Radar!")
                    .font(.title)
                    .fontDesign(.serif)
                    .bold()
                
                Text("Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum")
                    .font(.body)
                    .fontDesign(.serif)
                
                PrimaryActionButton("Next") {
                    // navigate to the next screen
                }
                .padding(.horizontal)
            }
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        }
        .ignoresSafeArea(.all, edges: .top)
        .statusBarHidden()
    }
}

extension VerticalAlignment {
    private struct IconToCarouselBottomAlignment: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            context[VerticalAlignment.center]
        }
    }
    
    static let iconToCarouselBottom = VerticalAlignment(
        IconToCarouselBottomAlignment.self
    )
}

At this point the view looks like this:

I want to push the Button in the Top VStack to the bottom of the screen and the red rounded rectangle to stay pinned to the bottom of the horizontal carousel (hence the custom alignment guide being introduced). The natural solution would be to add the Spacer(). However, for some reason when I do it, it results in the button going all the way down to outside of the screen, which means that the bottom safe area isn't being respected. Using the alignment parameter in the flexible frame also doesn't work the way I want it to. I suspect that custom alignment guide can cause this behavior but I can't find the way to fix it. Help me out, please.

Answered by ArsD in 846384022

@DTS Engineer Ended up making it work with a VStack after trying different other options. I dropped the custom alignment guide in favor of using offset. Here is the code if anyone is interested:

struct OnboardingGreeting: View {
    let carouselSpacing: CGFloat = 7
    let carouselItemSize: CGFloat = 110
    let carouselVelocities: [CGFloat] = [0.5, -0.25, 0.3, 0.2]
    let iconSize: CGFloat = 95
    
    var body: some View {
        VStack(spacing: 0) {
            VStack(spacing: carouselSpacing) {
                ForEach(carouselVelocities, id: \.self) { velocityValue in
                    InfiniteHorizontalCarousel(
                        albumNames: albums,
                        artistNames: artists,
                        itemSize: carouselItemSize,
                        itemSpacing: carouselSpacing,
                        velocity: velocityValue
                    )
                }
            }
            .overlay {
                LinearGradient(
                    stops: [
                        .init(color: .black.opacity(0.5), location: 0.07),
                        .init(color: .clear, location: 0.12)
                    ],
                    startPoint: .bottom,
                    endPoint: .top
                )
            }
            
            VStack(spacing: 8) {
                RoundedRectangle(cornerRadius: 18)
                    .fill(.red)
                    .frame(width: iconSize, height: iconSize)
                Text("Welcome to Music Radar")
                    .font(.title)
                    .fontDesign(.serif)
                    .bold()
                Text("It's your place to find new friends and share your tastes with the world. Enjoy!")
                    .font(.body)
                    .fontDesign(.serif)
            }
            .offset(y: -(iconSize/2))
            
            Spacer()
            
            PrimaryActionButton("Next") {
                // navigate to the next screen
            }
            .padding(.horizontal)
        }
        .ignoresSafeArea(edges: .top)
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        .statusBarHidden()
    }
}

and the final result:

I suspect that’s because the subviews are contained within a ZStack, and each view has a higher z-axis value. This is just a quick observation based on the snippet you provided. To better understand the issue, please provide a code snippet that reproduces it. I’m also curious about your choice to use a ZStack instead of a vertical stack in this case.

@DTS Engineer

Regarding the choice of ZStack, I figured that would be an appropriate choice for my use case, I have a carousel that acts like a background, then a LinearGradient as a middle layer and top layer VStack with the main content. If you have other suggestions, I would love to hear them. Also you said that higher z-axis value is probably the reason why my layout breaks. Could you elaborate on it a little bit more? I just don't understand how that would cause the issue in my case. Lastly, there is the snippet that reproduces the issue:

// Top VStack
            VStack(spacing: 0) {
                RoundedRectangle(cornerRadius: 18)
                    .fill(.red)
                    .frame(width: 95, height: 95)
                    .alignmentGuide(.iconToCarouselBottom) { context in
                        context.height / 2
                    }
                Text("Welcome to Music Radar!")
                    .font(.title)
                    .fontDesign(.serif)
                    .bold()
                
                // swiftlint:disable:next line_length
                Text("Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum Lorem Impsum")
                    .font(.body)
                    .fontDesign(.serif)
                
                Spacer()
                
                PrimaryActionButton("Next") {
                    // navigate to the next screen
                }
                .padding(.horizontal)
            }
            .border(.red)
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)  

Here is the result:

Accepted Answer

@DTS Engineer Ended up making it work with a VStack after trying different other options. I dropped the custom alignment guide in favor of using offset. Here is the code if anyone is interested:

struct OnboardingGreeting: View {
    let carouselSpacing: CGFloat = 7
    let carouselItemSize: CGFloat = 110
    let carouselVelocities: [CGFloat] = [0.5, -0.25, 0.3, 0.2]
    let iconSize: CGFloat = 95
    
    var body: some View {
        VStack(spacing: 0) {
            VStack(spacing: carouselSpacing) {
                ForEach(carouselVelocities, id: \.self) { velocityValue in
                    InfiniteHorizontalCarousel(
                        albumNames: albums,
                        artistNames: artists,
                        itemSize: carouselItemSize,
                        itemSpacing: carouselSpacing,
                        velocity: velocityValue
                    )
                }
            }
            .overlay {
                LinearGradient(
                    stops: [
                        .init(color: .black.opacity(0.5), location: 0.07),
                        .init(color: .clear, location: 0.12)
                    ],
                    startPoint: .bottom,
                    endPoint: .top
                )
            }
            
            VStack(spacing: 8) {
                RoundedRectangle(cornerRadius: 18)
                    .fill(.red)
                    .frame(width: iconSize, height: iconSize)
                Text("Welcome to Music Radar")
                    .font(.title)
                    .fontDesign(.serif)
                    .bold()
                Text("It's your place to find new friends and share your tastes with the world. Enjoy!")
                    .font(.body)
                    .fontDesign(.serif)
            }
            .offset(y: -(iconSize/2))
            
            Spacer()
            
            PrimaryActionButton("Next") {
                // navigate to the next screen
            }
            .padding(.horizontal)
        }
        .ignoresSafeArea(edges: .top)
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        .statusBarHidden()
    }
}

and the final result:

SwiftUI Layout Issue - Bottom safe area being ignored for seemingly no reason.
 
 
Q