0

I have three rectangles stacked on a ZStack, they all have their own coordinateSpace. I want to place the PinkRectangle in the upper left corner of the YellowRectangle, the corresponding code is: .position(x: proxy.frame(in: .named(" YellowRectangle")).minX, y: proxy.frame(in: .named("YellowRectangle")).minY). The rendering result shows that the PinkRectangle has position to the upper left corner of the screen. What is the cause of the error here?

enter image description here

Full example code:

struct ContentView: View {
    private let uiScreenWidth = UIScreen.main.bounds.width
    private let uiScreenHeight = UIScreen.main.bounds.height
        
    var body: some View {
        GeometryReader { proxy in
            ZStack {
                Rectangle()
                    .fill(.cyan)
                    .frame(width: uiScreenWidth * 0.9, height: uiScreenHeight * 0.9)
                    .coordinateSpace(name: "CyanRectangle")
                Rectangle()
                    .fill(.yellow)
                    .frame(width: uiScreenWidth * 0.6, height: uiScreenHeight * 0.6)
                    .coordinateSpace(name: "YellowRectangle")
                Rectangle()
                    .fill(.pink)
                    .frame(width: 100, height: 100)
                    .position(x: proxy.frame(in: .named("YellowRectangle")).minX,
                              y: proxy.frame(in: .named("YellowRectangle")).minY)
                    .coordinateSpace(name: "PinkRectangle")
            }
            .background(.gray)
        }
        .border(.black, width: 1)
    }
}
Janyee
  • 104
  • 8
  • Is there any reason not to use `.overlay(alignment: .topLeading)` instead of manually positioning it? – Timmy Aug 08 '23 at 12:48
  • See also [How does .position modifier change layout of parent Stack in SwiftUI?](https://stackoverflow.com/q/76783228/20386264) – Benzy Neez Aug 08 '23 at 13:48

1 Answers1

0

I think there are three reasons why it is not working as you expected:

  1. You seem to be expecting that GeometryProxy.frame gives you the frame of the container with the coordinate space. It is actually giving you the frame of the GeometryReader, which has the black border.

  2. I would assume that coordinate spaces do not work across stacks. So the item being positioned has to be inside the coordinate space you're trying to reference.

  3. The GeometryReader also needs to be inside the coordinate space you're trying to reference.

Of course, the item being positioned also needs to be inside the GeometryReader, otherwise the GeometryProxy will not be in scope.

What this all means, is that using .position in connection with a GeometryProxy.frame is only going to be useful when the GeometryReader is not occupying the full container space.

Looking at your example:

  • I would expect that minX and minY are delivered using the global coordinate space, because the yellow coordinate space is unknown to both the pink square and the GeometryProxy. This means, minX will be 0 and minY will be the height of the safe area inset at the top of the display.
  • The pink square is half out of view horizontally, because .position positions a view by its center.
  • It is just a coincidence that the pink square appears to align with the cyan rectangle on its top edge. If you run it on a device with different top insets (such as an iPhone 14 Pro) then it is clearly not aligned.
  • The ZStack is extending to the full bounds of the GeometryReader as a consequence of having an item in the ZStack being positioned using .position (this being the pink square). See How does .position modifier change layout of parent Stack in SwiftUI? for more elaboration.
  • The reason why the background is gray all the way to the screen edges is because when a background is set using .background() (in other words, with round parenthesis taking a ShapeStyle as parameter, as opposed to curly braces which would be a ViewBuilder function), it ignores safe areas by default.

So here is an adapted example:

  • The container for the yellow coordinate space is now a VStack
  • The GeometryReader containing the pink square has been moved inside this VStack.
  • A (new) orange Rectangle comes before the GeometryReader in the same VStack.
  • The horizontal position of the pink square uses minX from the cyan coordinate space.
  • The vertical position of the pink square uses minY from the yellow coordinate space.
  • An adjustment of half the square's width and half the square's height is added to the x and y positions. This adjustment is in order to position the square by its top-left corner, instead of by its center.
var body: some View {
    ZStack {
        Color.clear // expand ZStack to max bounds
        Group {
            VStack(spacing: 0) {
                Rectangle()
                    .foregroundColor(.orange)
                    .frame(height: 25)
                GeometryReader { proxy in
                    Rectangle()
                        .fill(.pink)
                        .frame(width: 100, height: 100)

                        // .position positions the centre of view,
                        // adjustment of half width and half height
                        // needed to position top-left corner
                        .position(

                            // The frame being referenced here is the frame
                            // of the GeometryReader (with black border),
                            // not the frame of the container.
                            x: proxy.frame(in: .named("CyanRectangle")).minX + 50,
                            y: proxy.frame(in: .named("YellowRectangle")).minY + 50
                        )
                        .coordinateSpace(name: "PinkRectangle")
                }
                .border(.black, width: 1)
            }
            .frame(width: uiScreenWidth * 0.6, height: uiScreenHeight * 0.6)
            .coordinateSpace(name: "YellowRectangle")
            .background { Color.yellow }
        }
        .frame(width: uiScreenWidth * 0.9, height: uiScreenHeight * 0.9)
        .coordinateSpace(name: "CyanRectangle") // must follow after frame!
        .background { Color.cyan }
    }
    .background(.gray)
}

CoordinateSpaces

Observations:

  • The pink square is shifted horizontally across by the same amount as the yellow rectangle is nested inside the cyan space, this being the offset of the GeometryReader inside the cyan coordinate space.
  • The pink square is shifted vertically down by the height of the orange rectangle, this being the offset of the GeometryReader inside the yellow coordinate space.
  • Something important that I discovered: the coordinateSpace needs to be defined after the frame is set on the container!

In conclusion, using coordinate spaces like this is quite a handy way of finding the dimensions of content outside the GeometryReader, when this other content is impacting the position of the GeometryReader inside a common container.

Benzy Neez
  • 1,546
  • 2
  • 3
  • 10