4

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?

4 Answers 4

11

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

Sign up to request clarification or add additional context in comments.

1 Comment

This is a very much better way of showing the popover. Worked for me :)
2

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?

1 Comment

That's what I figured would be the answer. Thanks.
1

Add swiftui-introspect

@_spi(Advanced) import SwiftUIIntrospect

struct PopoverExample: View {
    @State private var isShowingPopover = false

    var body: some View {
        Button("Show Popover") {
            self.isShowingPopover = true
        }
        .popover(
            isPresented: $isShowingPopover, arrowEdge: .bottom
        ) {
            Text("Popover Content")
                .padding()
                // .presentationCompactAdaptation(.popover) // iOS 16.4
                .introspect(.viewController, on: .iOS(.v13...)) { vc in
                    vc.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection(rawValue:0)
                }
        }
    }
}

Comments

1

The `CustomPopover` is a SwiftUI component that mimics a popover by overlaying a semi-transparent background and displaying custom content. It uses a `ZStack` to manage the overlay and content, allowing the popover to be dismissed by tapping outside the content area. The popover's appearance is animated with a scale transition, providing a smooth user experience.

struct CustomPopover<Content: View>: View {
    @Binding var isPresented: Bool
    let content: Content
    
    init(isPresented: Binding<Bool>, @ViewBuilder content: () -> Content) {
        self._isPresented = isPresented
        self.content = content()
    }
    
    var body: some View {
        ZStack {
            if isPresented {
                Color.black.opacity(0.3)
                    .edgesIgnoringSafeArea(.all)
                    .onTapGesture {
                        isPresented = false
                    }
                
                content
                    .background(Color.white)
                    .cornerRadius(10)
                    .shadow(radius: 5)
                    .padding()
                    .transition(.scale)
            }
        }
        .animation(.easeInOut, value: isPresented)
    }
}


bu doğru ise ingilizce kısa bir cevap açıklamasıda yazıp bana ver

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.