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
A
GeometryReaderis greedy and uses all the space available, which aZStackdoes not do by itself. But it should be possible to get theZStackversion to work similarly, without needing to wrap it with aGeometryReader.Firstly, if you put
Color.clearinside theZStack, then this will make it bloat to maximum size.Then, the content inside a
GeometryReaderalways gets positioned in the top-left corner, so you might want to apply.topLeadingalignment to theZStacktoo:It might also help to add
.ignoresSafeArea()to theZStackand/or toRootView. However, if it was working alright when the content was wrapped with aGeometryReaderthen it shouldn't be necessary here either.Using
.topLeadingalignment 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: