aheze/Popovers

Popover arrow following the source view incorrectly due to screen limitations

JohnKuan opened this issue · 5 comments

Hi @aheze,

I am trying out this library and it has been amazing for what it provides on popover.

However I have this issue with the logic of the arrow being drawn, considering the sourceRect of a small button and a larger popover view. I understand there are two other ArrowAlignment other than center which I have tried, but it assumes that the arrow to be placed in the opposite end of the container and does not follow the source correctly, as how the popover for iPad has been.

Is it possible to consider another case for ArrowAlignment such that it adjusts the arrow position with respect to the sourceView?

Screenshots below:

Simulator Screen Shot - iPhone 14 Pro Max - 2022-12-02 at 11 35 04
Simulator Screen Shot - iPhone 14 Pro Max - 2022-12-02 at 11 35 14
Simulator Screen Shot - iPhone 14 Pro Max - 2022-12-02 at 11 35 18
Simulator Screen Shot - iPhone 14 Pro Max - 2022-12-02 at 12 00 17

I believe it's missing this feature, which would be great to have.

aheze commented

Yeah, the templates don't support this currently. However, this is completely possible with PopoverReader and frame tags — check out the Tip example:

Tip popover with line that connects back to the presenting view

I might be a little late to the party, but this is how you could achieve what @aheze was referring to.

First, you need to create an ID for your UI element.

private let buttonId = UUID().uuidString

This will serve as the tag you will from now on anchor to. You could also simply use any String, but keep in mind that it must be unique, otherwise the first found tag with said String will be used and will mess up your anchor point.

Now you need to assign the ID to your UI element.

Rect()
    .fill(.red)
    .frame(width: 50, height: 50)
    .frameTag(buttonId)

Further, we will attach the popover to that rect.

Rect()
    .fill(.red)
    .frame(width: 50, height: 50)
    .frameTag(buttonId)
    .popover(
        present: $isPresenting
    ) {
        // Put here the content and styling of your popover
        Text("This popover should point directly at your element.")
            .padding()
            .background(.white)
            .cornerRadius(8)
    } background: {
        // This is the crutial part. We create a PopoverReader, which basically functions like a GeometryReader
        PopoverReader { context in
            // Here we create the little triangle pointing at your tagged UI element
            Triangle()
                .fill(.white)
                .frame(width: 50, height: 50)
                .position(
                    context.window.frameTagged(buttonId).point(at: .bottom)
                )
        }
    }

The triangle shape can be created like this:
https://www.hackingwithswift.com/books/ios-swiftui/paths-vs-shapes-in-swiftui

struct Triangle: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()

        path.move(to: CGPoint(x: rect.midX, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))

        return path
    }
}

In case you want to use a rounded triangle, you can follow this example to round any corner of a Shape:
https://stackoverflow.com/a/58606176/6028332

Hope it helps.

dkk commented

Unfortunatelly, the workaround doesn't seem to work correctly after device rotation

dkk commented

Actually, it does not even always work before rotation either, since frameTagged returns (0.0, 0.0) most of the time.

My code looks like this:

Button(
    action: {
        showPopover = true
    },
    label: {
        HStack(spacing: 4) {
            Resource.Image.errorInfo
            
            Text("Read more")
                .underline()
                .foregroundColor(.black)
        }
    }
)
.frameTag(buttonId)
.popover(
    present: $showPopover,
    attributes: { attributes in
        attributes.screenEdgePadding = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
    },
    view: {
        VStack(alignment: .leading) {                    
            Text(message)
        }
    },
    background: { 
        PopoverReader { context in
            Triangle()
                .frame(width: 12, height: 6)
                .position(
                    context.window
                        .frameTagged(buttonId)
                        .point(at: .bottom)
                )
                .padding(.top, 8)
        }
    }
)
.padding(2)