Search code examples
iosswiftuipopover

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?


Solution

  • 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