iOS SwiftUI Need to Display Popover Without "Arrow"

83 views Asked by At

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?

2

There are 2 answers

1
Benzy Neez On BEST ANSWER

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.

Here is an example to show how it can work:

struct ContentView: View {

    enum PopoverTarget {
        case text1
        case text2
        case text3
    }

    @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)
                }
                .matchedGeometryEffect(
                    id: popoverTarget,
                    in: nsPopover,
                    properties: .position,
                    isSource: false
                )
        }
    }

    private func popoverPlaceholder(
        target: PopoverTarget,
        xOffset: CGFloat = 0,
        yOffset: CGFloat = 0
    ) -> some View {
        Color.clear
            .matchedGeometryEffect(id: target, in: nsPopover)
            .offset(x: xOffset, y: yOffset)
    }

    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) }
                    .background { popoverPlaceholder(target: .text1, yOffset: 70) }
                    .padding(.top, 50)
                    .padding(.leading, 100)
                    .frame(maxWidth: .infinity, alignment: .leading)

                Text("Text 2")
                    .padding()
                    .background(.orange)
                    .onTapGesture { showPopover(target: .text2) }
                    .background { popoverPlaceholder(target: .text2, xOffset: -40, yOffset: -70) }
                    .padding(.top, 100)
                    .padding(.trailing, 40)
                    .frame(maxWidth: .infinity, alignment: .trailing)

                Spacer()

                Text("Text 3")
                    .padding()
                    .background(.green)
                    .onTapGesture { showPopover(target: .text3) }
                    .background { popoverPlaceholder(target: .text3, yOffset: -70) }
                    .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

1
Mahi Al Jawad On

Currently there is no way of showing .popOver without an arrow in SwiftUI. Because it will show any of the Edge arrow definitely. You can check the declaration:

public func popover<Item, Content>(item: Binding<Item?>, attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), arrowEdge: Edge = .top, @ViewBuilder content: @escaping (Item) -> Content) -> some View where Item : Identifiable, Content : View

But you can achieve this using UIKit. For UIKit I'd refer this question: Can I remove the arrow in the popover view?