swiftmacosswiftui

SwiftUI Global CoordinateSpace Issue: Circle Not Aligning with View Origin


I am reading the frame of a white-colored view in my macOS SwiftUI code. I want to place a circle at the origin of this white-colored view. I am using .global as the CoordinateSpace since both the white view and the red circle have access to the global coordinate space and are in the same hierarchy.

I should be able to successfully position the circle at the origin of the white view, but for some reason—I believe due to my app's title bar being 28.0 points high—the circle’s Y position is not aligning as expected.

How can I solve this issue? This doesn't make sense to me because we are dealing with the global CoordinateSpace, and for a given CGPoint in the global coordinate system, there should be only one exact position.

import SwiftUI

struct ContentView: View {
    @State private var rect: CGRect = CGRect()
    var body: some View {
        ZStack {

            Color.white
                .frame(width: 300.0, height: 300.0)
                .background(
                    GeometryReader { geometryValue in
                        let frame = geometryValue.frame(in: .global)
                        Color.clear
                            .onAppear {
                                print("onAppear global:", frame)
                                rect = frame
                            }
                            .onChange(of: frame) { newValue in
                                print("onChange global:", newValue)
                                rect = newValue
                            }
                        
                    }
                )
                .padding(25.0)
                .border(Color.black)

            Circle()
                .fill(Color.red)
                .opacity(0.75)
                .frame(width: 50.0, height: 50.0)
                .position(x: rect.origin.x, y: rect.origin.y)

        }

    }
}

enter image description here


Solution

  • It seems like your misunderstanding is that you think .position takes in a coordinate that is in the global coordinate space. This is not true.

    .position actually expands the logical frame of the view to fill all the available space, much like a Rectangle or Color. The x and y coordinates you pass to .position is relative to this frame. Try adding a .border(.red) after .position(...) and see the bounds of the frame.

    You can see this even more clearly with this code:

    HStack {
        Color.green
        Circle()
            .fill(Color.red)
            .opacity(0.75)
            .frame(width: 50, height: 50)
            .position(x: 0, y: 0)
        Color.blue
    }
    

    All three views in the HStack are views that try to take up as much space as possible, and so HStack will resize them to let them fill one third of the horizontal space each. The circle ends up being positioned at the top right of the green, instead of at the global (0, 0), which is the top left of the green.


    Going back to your code, since the circle is in a ZStack, .position will expand to cover the whole ZStack. The ZStack does not extend beyond the title bar by default. On the other hand, the .global coordinate space does include the title bar, as you have found out.

    To position the red circle correctly, the coordinate space you should use is that of the ZStack.

    ZStack {
        Color.white
            .frame(width: 300.0, height: 300.0)
            // on macOS 13+, you can use onGeometryChange which is much less boilerplate!
            .onGeometryChange(for: CGRect.self) {
                $0.frame(in: .named("X"))
            } action: {
                rect = $0
            }
            // for before macOS 13:
            /*
            .background {
                GeometryReader { geometryValue in
                    let frame = geometryValue.frame(in: .named("X"))
                    Color.clear
                        .onAppear {
                            print("onAppear global:", frame)
                            rect = frame
                        }
                        .onChange(of: frame) { newValue in
                            print("onChange global:", newValue)
                            rect = newValue
                        }
                    
                }
            }
            */
            .padding(25)
            .border(Color.black)
    
        Circle()
            .fill(Color.red)
            .opacity(0.75)
            .frame(width: 50, height: 50)
            .position(x: rect.origin.x, y: rect.origin.y)
    }
    .coordinateSpace(.named("X")) // name the coordinate space of the ZStack!
    // before macOS 14:
    // .coordinateSpace(name: "X")
    

    Alternatively, if you add .ignoresSafeArea() to the ZStack, then the ZStack (and its coordinate space) will coincide with that of the global coordinate space (since the title bar counts as an "unsafe area"), and using the .global coordinate space will position the circle correctly as well.