Search code examples
swiftuigeometryreader

SwiftUI overlay alignment for horizontal centering


I need view A to be centered on the screen (vertically and horizontally). I need view B to appear 30px below view A, also centered horizontally. But A must remain in the center of the screen, without moving upward when B is added. I can accomplish most of this by using .overlay and GeometryReader:

struct ContentView: View {
        
    var body: some View {
        VStack {
            ZStack {
                Rectangle()
                    .fill(.red)
                    .frame(width: 200, height: 100)
                    .overlay {
                        GeometryReader {geo in
                            Rectangle()
                                .fill(.blue)
                                .frame(width: 100, height: 100)
                                .offset(y: geo.size.height + 30)
                        }
                    }
            }
        }
    }
}

enter image description here

I don't know how to center the blue Rectangle horizontally though. I don't necessarily know its width, although it's specified here for visual purposes. Assuming I don't know the exact frame values for either view, how I can I position B 30px below A while keeping A in the center of the screen? And is there a better view structure for accomplishing this?


Solution

  • You could just use a VStack with a hidden copy of the blue rectangle above the red one:

    struct ContentView: View {
        
        private var blueRectangle: some View {
            Rectangle()
                .fill(.blue)
                .frame(width: 100, height: 100)
        }
        
        var body: some View {
            VStack(spacing: 30) {
                blueRectangle.hidden()
                Rectangle()
                    .fill(.red)
                    .frame(width: 200, height: 100)
                blueRectangle
            }
        }
    }
    

    Screenshot

    Alternatively, if the blue rectangle is not expected to be wider than the red one then here's how you could do it with an overlay:

    var body: some View {
        Rectangle()
            .fill(.red)
            .frame(width: 200, height: 100)
            .overlay(alignment: .top) {
                GeometryReader { proxy in
                    blueRectangle
                        .frame(maxWidth: proxy.size.width)
                        .fixedSize(horizontal: false, vertical: true)
                        .offset(y: proxy.size.height + 30)
                }
            }
    }