Importance of wrapping SwiftUI View in GeometryReader for landscape orientation

153 Views Asked by At

After years of development using UIKit I decided to get my hands on SwiftUI. And I found one very odd thing, that cost me at least 6 hours of hair pulling.

Objective: I have an application that works in both portrait and landscape modes.

Portrait mode: Everything is totally fine, UI is rendered exactly as I expect.

Landscape mode: When I rotate my phone to landscape mode, all UI seems to have an offset that is out of screen boundaries. While I see my elements aligned horizontally properly, I do not see top part of the screen. It doesn't matter, if I launch the app having a phone already in landscape, or if I launch an app in portrait mode and then rotate - effect is exactly the same.

Here is my ContentView - Root view, that is a start of my UI

struct ContentView: View {
    
    @State var isActive: Bool = false
    
    private let container: DIContainer
    
    init(container: DIContainer) {
        self.container = container
        self.isActive = isActive
    }
    
    var body: some View {
            ZStack {
                if self.isActive {
                    
                    RootView().inject(container)
                    
                } else {
                    LinearGradient(colors: [.blue, Color("light_blue")],
                                   startPoint: .top,
                                   endPoint: .center)
                    .edgesIgnoringSafeArea(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/)
                    
                    Text("HELLO")
                        .font(.system(size:36))
                        .foregroundColor(Color.white)
                }
                
            }.onAppear {
                DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
                    
                    withAnimation {
                        self.isActive = true
                    }
                }
            }
    }
}

So in order to debug this INCREDIBLY annoying issue, I decided to wrap my ContentView in GeometryReader to see what offset does it have, etc. So I did this

struct ContentView: View {
    
    @State var isActive: Bool = false
    
    private let container: DIContainer
    
    init(container: DIContainer) {
        self.container = container
        self.isActive = isActive
    }
    
    var body: some View {
        GeometryReader { geometry in
            ZStack {
                if self.isActive {
                    
                    RootView().inject(container)
                    
                } else {
                    LinearGradient(colors: [.blue, Color("light_blue")],
                                   startPoint: .top,
                                   endPoint: .center)
                    .edgesIgnoringSafeArea(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/)
                    
                    Text("HELLO")
                        .font(.system(size:36))
                        .foregroundColor(Color.white)
                }
                
            }.onAppear {
                DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
                    
                    withAnimation {
                        self.isActive = true
                    }
                }
            }
        }
    }
}

And would you believe - it fixed an issue! Once I wrapped my ContentView in GeometryReader, application started to render properly in all modes - portrait and landscape. So basically - out of pure luck I have fixed this issue.

My question is - why do I need to wrap ContentView in Geometry Reader so the SwiftUI will be rendered properly? The reason I am asking is - I searched all Google, Youtube, etc and I did not find this recomendation anywhere! So my assumption is that something is horribly wrong in my SwiftUI code that is why I need to implement this workaround. Because I refuse to believe that everyone who have landscape-supportive apps dealt with the same issue silently and didn't share this important information.

UPD: If I lock the app in only landscape mode, it is still rendered inproperly with huge offset to the top. If I wrap ContentView in GeometryReader, it renders fine

1

There are 1 best solutions below

0
Benzy Neez On

A GeometryReader is greedy and uses all the space available, which a ZStack does not do by itself. But it should be possible to get the ZStack version to work similarly, without needing to wrap it with a GeometryReader.

Firstly, if you put Color.clear inside the ZStack, then this will make it bloat to maximum size.

Then, the content inside a GeometryReader always gets positioned in the top-left corner, so you might want to apply .topLeading alignment to the ZStack too:

ZStack(alignment: .topLeading) { // alignment added
    Color.clear // added
    if self.isActive {
        RootView().inject(container)
    } else {
        // other content as before
    }
}

It might also help to add .ignoresSafeArea() to the ZStack and/or to RootView. However, if it was working alright when the content was wrapped with a GeometryReader then it shouldn't be necessary here either.

Using .topLeading alignment will cause the initial text message to appear in the top-left corner too, so to resolve this you could apply maximum width and height to the text:

Text("HELLO")
    .font(.system(size:36))
    .foregroundColor(Color.white)
    .frame(maxWidth: .infinity, maxHeight: .infinity) // added