swiftswiftui

Why doesn't my SwiftUI coordinate space use .zero as its origin, and how can I make it the base coordinate space of the app?


Why does a custom coordinate space (e.g., "White Screen") in SwiftUI not use .zero as its origin when measured via GeometryReader? Instead, it appears to inherit its position from the global or parent coordinate space. How can I ensure that the custom coordinate space defines its origin at .zero and behaves as the base frame for measurements?

Currently, I am brute-forcing the origin to get what I need, but if this is the only way to set my coordinate space to zero at the top-left, what is the use case for it? I mean, it’s just inheriting from a parent. So, if I’m going to introduce my custom coordinate space to my app, I want all UI elements to inherit from it and for it to become my new custom global system.

P.S. The function that returns the string does not update the view when the state value changes. I'm not sure why; you have to resize the window to see the updated string in the text.

import SwiftUI

struct ContentView: View {
    
    private let coordinateSpaceName: String = "White Screen"
    
    @State private var screenCGRect: CGRect = CGRect()
    
    @State private var  bruteForce: Bool = Bool()

    var body: some View {
        
        
        VStack(spacing: 10.0) {
                        
            Text(CGRectdescription(id: coordinateSpaceName, rect: screenCGRect)).font(.title3)
            
            GeometryReader { geometryValue in
                
                ZStack {
                    
                    Color.white.opacity(0.5).border(Color.black)
                        .coordinateSpace(name: coordinateSpaceName)
                    
                    Color.red
                        .frame(width: 1.0, height: screenCGRect.size.height)
                    
                    Color.red
                        .frame(width: screenCGRect.size.width, height: 1.0)

                }
                .onAppear {
                    
                    if (bruteForce) {
                        screenCGRect = CGRect(origin: .zero, size: geometryValue.size)
                    }
                    else {
                        screenCGRect = geometryValue.frame(in: .named(coordinateSpaceName))
                    }

                }
                .onChange(of: geometryValue.frame(in: .named(coordinateSpaceName))) { newValue in

                    if (bruteForce) {
                        screenCGRect = CGRect(origin: .zero, size: newValue.size)
                    }
                    else {
                        screenCGRect = newValue
                    }
                }

            }
            
            Button(bruteForce ? "Geometry Reader Origin" : "Brute Force Origin") {
                bruteForce.toggle()
            }

        }
        .padding()
        
    }
    
    func CGRectdescription(id: String, rect: CGRect) -> String {
        return id + ": (" + String(format: "%.2f", rect.origin.x) + ", " + String(format: "%.2f", rect.origin.y) + ", " + String(format: "%.2f", rect.size.width) + ", " + String(format: "%.2f", rect.size.height) + ")"
    }
    
}

Solution

  • Coordinate spaces declared by .coordinateSpace(...) are only visible to the children of that modifier. The GeometryReader cannot see .coordinateSpace(name: coordinateSpaceName) because the GeometryReader is its parent view. As a result, it uses the coordinate space of the window as a fallback.

    If you put .coordinateSpace(...) on the GeometryReader, then the code works as expected. The origin returned by frame(in:) is (0, 0).

    You can also put it on the VStack, in which case frame(in:) will return an origin of (0, 29). 29pt happens to be the distance between the top of the VStack and the top of the GeometryReader.