SwiftUI LandScape Orientation, Geometry Reader and View does not look like expected

Hello together,

following issue. I would like to have the UI look like that in landscape mode:

in the middle, there should be a recangle in ratio 4:3. the length should be derived by the available screen height. The space left and right shall have the same size and are VStacks for further use of button elements. So the rectangle is centered to the landscape screen. below you see the code.

import Foundation
import SwiftUI

// MARK: - ScoreboardView
struct ScoreboardView: View {

@Environment(\.horizontalSizeClass) var hSizeClass
@Environment(\.verticalSizeClass) var vSizeClass

// View
var body: some View {
    
    // device orientation: portrait
    if hSizeClass == .compact && vSizeClass == .regular {
        ScoreboardViewPortrait()              // pass SteteObject scoreboard to Portrait view
    }
    //device orientation: landscape
    else {
        ScoreboardViewLandscape()
    }
}

}

// MARK: - Preview
struct ScoreboardView_Previews: PreviewProvider {

static var previews: some View {
    Group {
        ScoreboardView()
    }
}

}

struct ScoreboardViewPortrait: View {

// View - device orientation: portrait
var body: some View {
    Text("Hello World")
}

}

struct ScoreboardViewLandscape: View {

// View - device orientation: landscape
var body: some View {
    
    HStack {
        VStack {
            Text("Test")
        }
        
        GeometryReader { geometry in
            ZStack {
                // get display sizes
                let screenWidth = UIScreen.main.bounds.width
                let screenHeigth = UIScreen.main.bounds.height
                let screenX = Double(geometry.size.width)
                let screenY = Double(geometry.size.height)
                let topPadding = 10.0
                let bottomPadding = 0.0
                
                // LED-Wall size
                let rectY = (screenHeigth+screenY)/2.0-topPadding-bottomPadding
                let rectX = 4*rectY/3
                
                
                // LED-Scoreboard
                ZStack {
                    // Scoreboard background
                    Rectangle()
                        .frame(width: rectX, height: rectY)
                        .opacity(0.1)
                        .background(LinearGradient(gradient: Gradient(colors: [.white, Color(.red), Color(.blue)]), startPoint: .top, endPoint: .bottom))
                    
                    // Scoreboard grid ( 4 x 4, ratio 4:3)
                ForEach(0..<5) { idx in
                        Path({ path in
                            let x = (screenX-rectX)/2 + Double(idx)*rectX/4
                            path.move(to: CGPoint(x: x, y: topPadding))
                            path.addLine(to: CGPoint(x: x, y: rectY+topPadding))
                            path.move(to: CGPoint(x: (screenX-4*rectY/3)/2, y: Double(idx)*rectY/4+topPadding))
                            path.addLine(to: CGPoint(x: (screenX+4*rectY/3)/2, y: Double(idx)*rectY/4+topPadding))
                        })
                            .stroke(Color.black, style: StrokeStyle(lineWidth: 1, dash: [3]))
                    }
                }
                //.edgesIgnoringSafeArea(.all)
                
                

            }
        }
            
        VStack{
            Text("Test")
        }
    }
    .background(Color.gray)
    .edgesIgnoringSafeArea(.all)
}

}

And that’s what the simulator shows to me:

Maybe you recognize the problems:
1.) the text in the left VStack does not appear
2.) rectangle and grid don’t match

The variations of the rectangle and the grid are even bigger, when I uncomment “.edgesIgnoringSafeArea(.all)”. See below

The text in the left Stack shows up, the rectangle goes from top to bottom, the grid looks like it should be. It get’s more interesting, when “bottomPadding=10.0” (get’s a value). But it’s still not what I’m expecting …

Due to the fact, that the rectangle has the same values (rectX, rectY) as the grid and .frame(rectX,rectY) should center it on the available UI space, I can’t explain, why the rectangle and the grid don’t match in any case.

Any explanation or better coding tips are appreciated very much!

:wink: Peter

Hi Peter,

I downloaded your project and opened it with Xcode 13.1 but it just would not compile so I commented out the code in the ScoreboardViewLandscape and it then complied.

I tried to figure out what you were trying to do in the code but it was all a bit confusing so I came up with my own version based on your layout and some of your code once I had surmised what you were attempting to do in configuring the grid.

This is my version of the View

struct ScoreboardViewLandscape: View {
    var body: some View {

        let screenWidth = UIScreen.main.bounds.width
        let screenHeight = UIScreen.main.bounds.height
        let rectangleWidth = screenHeight * 4 / 3
        let edgeWidth = (screenWidth - rectangleWidth) / 2
        HStack {
            VStack {
                Text("Left View")
            }
            .padding(.leading, 33)
            .frame(width: edgeWidth)

            ZStack {
                GeometryReader { gr in
                    Rectangle()
                        .fill(LinearGradient(gradient: Gradient(colors: [Color.white, Color.red, Color.blue]), startPoint: .top, endPoint: .bottom))
                        .opacity(1)
                        .frame(width: rectangleWidth, height: screenHeight)
                        .position(x: gr.size.width / 2, y: gr.size.height / 2)

                }
                let rectY = screenHeight
                let rectX = rectangleWidth

                ForEach(0..<5) { idx in
                    Path( { path in
                        let x = Double(idx) * rectX / 4 - 9
                        let y = Double(idx) * rectY / 4
                        path.move(to: CGPoint(x: x, y: 0))
                        path.addLine(to: CGPoint(x: x, y: rectY))
                        path.move(to: CGPoint(x: -9, y: y))
                        path.addLine(to: CGPoint(x: rectX - 9, y: y))
                    })
                        .stroke(Color.black, style: StrokeStyle(lineWidth: 1, dash: [3]))
                }

            }

            VStack {
                Text("Right View")
            }
            .frame(width: edgeWidth)
        }
        .edgesIgnoringSafeArea([.bottom, .leading, .trailing])
    }
}

To say that it was tricky is an understatement.
The odd thing about the centre Rectangle is that when you place your cursor on the code in the editor window the bounds of the Rectangle are highlighted with the edges shown as in the image below.

For whatever the reason, that edge is 9 pixels in from the left and right hand edges. If you don’t adjust the grid position by that amount the grid will not be in the right position. This only affects the x position with regard to constructing the grid, but I ran the code on the iPod Touch and a number of different simulator sizes and the layout is correct on all of them.

This might not be the correct approach but I have no idea what the right approach is.

The other thing is that any content that is on the Left of the grid panel needs to be padded by 33 so that the Text or anything else does not go under the notch.

This is the resulting iPhone 11 simulator output:

It was an interesting challenge.

2 Likes

Hy Chris,

thanks for your time and your trials. Do you think, that’s a bug. Shall i report this phenomenon to Apple?

And i have observed a couple of problems with the geometry reader. Sometimes my code gets compiled, sometimes not. I have the feeling, that as soon the calculations get a little bit more complex - whatever complex means by just using ± * and / - the compiler stops with nonsaying errors. Second phenomenon: with Xcode 13.1 there is a memory problem in the context with the geometry reader. A service obviously doesn’t releases internal memory again. My iMac complains 64GByte RAM as full … Therefore i tried using XCode 13.2 RC.

have you observed such problems too?

:wink: Peter

@peter_luger

Is it a bug? I was just about to say I don’t know then I suddenly realised what that gap is caused by.

There is a standard gap between HStack objects so I added the parameter HStack(spacing: 0) and then removed the adjustment values I added and, voila, fixed.

Code:

struct ScoreboardViewLandscape: View {
    var body: some View {

        let screenWidth = UIScreen.main.bounds.width
        let screenHeight = UIScreen.main.bounds.height
        let rectangleWidth = screenHeight * 4 / 3
        let edgeWidth = (screenWidth - rectangleWidth) / 2
        HStack(spacing: 0) {
            VStack {
                Text("Left View")
            }
            .padding(.leading, 33)
            .frame(width: edgeWidth)

            ZStack {
                GeometryReader { gr in
                    Rectangle()
                        .fill(LinearGradient(gradient: Gradient(colors: [Color.white, Color.red, Color.blue]), startPoint: .top, endPoint: .bottom))
                        .opacity(1)
                        .frame(width: rectangleWidth, height: screenHeight)
                        .position(x: gr.size.width / 2, y: gr.size.height / 2)

                }

                let rectY = screenHeight
                let rectX = rectangleWidth

                ForEach(0..<5) { idx in
                    Path( { path in
                        let x = Double(idx) * rectX / 4
                        let y = Double(idx) * rectY / 4
                        path.move(to: CGPoint(x: x, y: 0))
                        path.addLine(to: CGPoint(x: x, y: rectY))

                        path.move(to: CGPoint(x: 0, y: y))
                        path.addLine(to: CGPoint(x: rectX, y: y))
                    })
                        .stroke(Color.black, style: StrokeStyle(lineWidth: 1, dash: [3]))
                }

            }

            VStack {
                Text("Right View")
            }
            .frame(width: edgeWidth)
        }
        .edgesIgnoringSafeArea([.bottom, .leading, .trailing])
    }
}

When I opened your project for the first time, the compiler was struggling to figure out what was going on. The fans on my MacBook Pro went crazy and it got very hot. I had to shut down Xcode because Activity Monitor was showing huge kernel process CPU usage. Clearly something was wrong. That’s why I started from the beginning in a new project to build it out little by little.

Yes, if you have complex mathematical calculations the compiler will sometimes struggle so what you need to do is work out a way to simplify it like I did. I don’t think this gets worse inside a GeometryReader although I can’t be sure.

Memory usage in my version was only 0.08% of the 64Gb that I have so absolutely no issue there. See the screenshot attached.

1 Like

Take the calculations out of the View's body property. That should help with the calculation issues. You should really try to never do calculations inside the body. Also, pull out stuff into separate Views, like the Path that you construct in a ForEach.

1 Like

Yeah absolutely agree Patrick.

This seems to work reasonably well @peter_luger :

struct ScoreboardViewLandscape: View {

    let screenWidth = UIScreen.main.bounds.width
    let screenHeight = UIScreen.main.bounds.height
    var rectangleWidth: Double = 0
    var edgeWidth: Double = 0

    init() {
        rectangleWidth = screenHeight * 4 / 3
        edgeWidth = (screenWidth - rectangleWidth) / 2
    }

    var body: some View {

        HStack(spacing: 0) {
            VStack {
                Text("Left View")
            }
            .padding(.leading, 33)
            .frame(width: edgeWidth)

            ZStack {
                GeometryReader { gr in
                    Rectangle()
                        .fill(LinearGradient(gradient: Gradient(colors: [Color.white, Color.red, Color.blue]), startPoint: .top, endPoint: .bottom))
                        .opacity(1)
                        .frame(width: rectangleWidth, height: screenHeight)
                        .position(x: gr.size.width / 2, y: gr.size.height / 2)

                }

                ForEach(0..<5) { idx in
                    GridView(rectangleWidth: rectangleWidth, screenHeight: screenHeight, idx: idx)
                }

            }

            VStack {
                Text("Right View")
            }
            .frame(width: edgeWidth)
        }
        .edgesIgnoringSafeArea([.bottom, .leading, .trailing])
    }
}
struct GridView: View {
    var rectangleWidth: Double
    var screenHeight: Double
    var idx: Int

    var body: some View {
        Path( { path in
            let x = Double(idx) * rectangleWidth / 4
            let y = Double(idx) * screenHeight / 4
            path.move(to: CGPoint(x: x, y: 0))
            path.addLine(to: CGPoint(x: x, y: screenHeight))

            path.move(to: CGPoint(x: 0, y: y))
            path.addLine(to: CGPoint(x: rectangleWidth, y: y))
        })
            .stroke(Color.black, style: StrokeStyle(lineWidth: 1, dash: [3]))
    }
}
1 Like

great job guys! thank you so much guys.

i will pull my hat off, if you can explain the padding-necessity (of 33) for the left VStack too! I would like to understand the logic of SwiftUI …

Because you’re ignoring the safe are insets on the top edge (left when rotated). There’s a notch there. As you can see in your first screenshot, the notch covers up anything there.

1.) the text in the left VStack does not appear

It does appear in the View, but it’s hidden by the physical notch.

interesting: the code works now without the padding (as I expected it - because the text was longer than the notch)

The only thing is that if your Text() contains a very long string you will find the left edge will still disappear under the notch. In fact any content that fills the entire View inside that VStack will have part of it under the notch without the padding().

With padding()

Without padding()