Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS SwiftUI Need to Display Popover Without "Arrow"

I need to display a list of selections for the user to choose. I have examined Menu, .contextMenu(), and .popover(). While all three of these work fine, I cannot display what I need to show or I cannot style them to meet design needs. For example:

  • Menu
    • Only accepts a StringLiteral argument. I need it to accept a View.
    • List only displays Label with text and image. I need it to accept a View. When I convert the View to an Image it clips to top and bottom.
  • .contextMenu()
    • I can run this on a view, but the List has the same Label problems when I attempt to display an image.
    • Only displays list with longPress. It needs to be a tap.
  • .popover()
    • Performs everything I need for display except that in iOS it displays an arrow pointing to the parent view. I cannot have an arrow.

At this point it looks like popover is the most favorable option if I can set it up so the arrow is not displayed. From what I understand from the documentation only macOS is allowed to hide the arrow.

Is there a way to show .popover() without the arrow in iOS?

like image 994
Cyklist Avatar asked Sep 05 '25 00:09

Cyklist


1 Answers

You could always build your own popover. The following techniques could be used:

  • Show the popover as the top layer in a ZStack.
  • Use .matchedGeometryEffect for positioning.

Different anchors can be used to control exactly how the popover is positioned relative to a target. For example, to position the popover horizontally centered below a target, the target would use an anchor of .bottom and the popover itself would use an anchor of .top.

This shows it working:

struct ContentView: View {

    enum PopoverTarget {
        case text1
        case text2
        case text3

        var anchorForPopover: UnitPoint {
            switch self {
            case .text1: .top
            case .text2: .bottom
            case .text3: .bottom
            }
        }
    }

    @State private var popoverTarget: PopoverTarget?
    @Namespace private var nsPopover

    @ViewBuilder
    private var customPopover: some View {
        if let popoverTarget {
            Text("Popover for \(popoverTarget)")
                .padding()
                .foregroundStyle(.gray)
                .background {
                    RoundedRectangle(cornerRadius: 10)
                        .fill(Color(white: 0.95))
                        .shadow(radius: 6)
                }
                .padding()
                .matchedGeometryEffect(
                    id: popoverTarget,
                    in: nsPopover,
                    properties: .position,
                    anchor: popoverTarget.anchorForPopover,
                    isSource: false
                )
        }
    }

    private func showPopover(target: PopoverTarget) {
        if popoverTarget != nil {
            withAnimation {
                popoverTarget = nil
            } completion: {
                popoverTarget = target
            }
        } else {
            popoverTarget = target
        }
    }

    var body: some View {
        ZStack {
            VStack {
                Text("Text 1")
                    .padding()
                    .background(.blue)
                    .onTapGesture { showPopover(target: .text1) }
                    .matchedGeometryEffect(id: PopoverTarget.text1, in: nsPopover, anchor: .bottom)
                    .padding(.top, 50)
                    .padding(.leading, 100)
                    .frame(maxWidth: .infinity, alignment: .leading)

                Text("Text 2")
                    .padding()
                    .background(.orange)
                    .onTapGesture { showPopover(target: .text2) }
                    .matchedGeometryEffect(id: PopoverTarget.text2, in: nsPopover, anchor: .topLeading)
                    .padding(.top, 100)
                    .padding(.trailing, 40)
                    .frame(maxWidth: .infinity, alignment: .trailing)

                Spacer()

                Text("Text 3")
                    .padding()
                    .background(.green)
                    .onTapGesture { showPopover(target: .text3) }
                    .matchedGeometryEffect(id: PopoverTarget.text3, in: nsPopover, anchor: .top)
                    .padding(.bottom, 250)
            }
            customPopover
                .transition(
                    .opacity.combined(with: .scale)
                    .animation(.bouncy(duration: 0.25, extraBounce: 0.2))
                )
        }
        .foregroundStyle(.white)
        .contentShape(Rectangle())
        .onTapGesture {
            popoverTarget = nil
        }
    }
}

Animation

like image 190
Benzy Neez Avatar answered Sep 07 '25 20:09

Benzy Neez