Search code examples
swiftuicontextmenu

SwiftUI contextmenu wrong location


I am trying to add a contextmenu to images I display, but when I longpress, the contextmenu opens in a wrong location (see image)

The code I use is:

ForEach(self.document.instruments) { instrument in
    Image(instrument.text)
        .resizable()
        .frame(width: 140, height: 70)
        .position(self.position(for: instrument, in: geometry.size))
        .gesture(self.singleTapForSelection(for: instrument))
        .gesture(self.dragSelectionInstrument(for: instrument))
        .shadow(color: self.isInstrumentSelected(instrument) ? .blue : .clear, radius: 10 * self.zoomScale(for: instrument))
        .contextMenu {
            Button {
                print("Deleted selected")
            } label: {
                Label("Delete", systemImage: "trash")
            }
        }
}

Where the context menu is and where it should be

Update of issue

The issue I still face (as my last comment), is that when I drag an image, there seems to be a delay, as the image remains on its position for a short moment and then moves to the dragged position. I already found out that when I have the contextMenu in the code, the delay dragging happens, when commented out, it works fine.

This is the code I have:

ForEach(self.document.instruments) { instrument in
    Image(instrument.text)
        .resizable()
        .frame(width: 140, height: 70)
        .contextMenu {
            Button {
                print("Deleted selected")
            } label: {
                Label("Delete", systemImage: "trash")
            }
        }
        .position(self.position(for: instrument, in: geometry.size))
        .shadow(color: self.isInstrumentSelected(instrument) ? .blue : .clear, radius: 10 * self.zoomScale(for: instrument))
        .gesture(self.singleTapForSelection(for: instrument))
        .gesture(self.dragSelectionInstrument(for: instrument))
}

I even tried moving the gestures above the contextmenu, but didn't work.

Please help

Latest Update

Nm, it was only on the simulator, on my physical iPad it works fine


Solution

  • The problem is that your code applies the contextMenu modifier after the position modifier.

    Let's consider this slightly modified example:

    ZStack {
        GeometryReader { geometry in
            ForEach(self.document.instruments, id: \.id) { instrument in
                Image(instrument.text)
                    .frame(width: 140, height: 70)
                    .position(self.position(for: instrument, in: geometry.size))
                    .contextMenu { ... }
            }
        }
    }
    

    In the SwiftUI layout system, a parent view is responsible for assigning positions to its child views. A view modifier acts as the parent of the view it modifies. So in the example code:

    • ZStack is the parent of GeometryReader.
    • GeometryReader is the parent of ForEach.
    • ForEach is the parent of contextMenu.
    • contextMenu is the parent of position.
    • position is the parent of frame.
    • frame is the parent of Image.
    • Image is not a parent. It has no children.

    (Sometimes the parent is called the “superview” and the child is called the “subview”.)

    When contextMenu needs to know where to draw the menu on the screen, it looks at the position given to it by its parent, the ForEach, which gets it from the GeometryReader, which gets it from the ZStack.

    When Image needs to know where to draw its pixels on the screen, it looks at the position given to it by its parent, which is the frame modifier, and the frame modifier gets the position from the position modifier, and the position modifier modifies the position given to it by the contextMenu.

    This means that the position modifier does not affect where the contextMenu draws the menu.

    Now let's rearrange the code so contextMenu is the child of position:

    ZStack {
        GeometryReader { geometry in
            ForEach(self.document.instruments, id: \.id) { instrument in
                Image(instrument.text)
                    .frame(width: 140, height: 70)
                    .contextMenu { ... }
                    .position(self.position(for: instrument, in: geometry.size))
            }
        }
    }
    

    Now the contextMenu gets its position from the position modifier, which modifies the position given to it by the ForEach. So in this scenario, the position modifier does affect the where the contextMenu draws the menu.