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:
I believe it's missing this feature, which would be great to have.
Yeah, the templates don't support this currently. However, this is completely possible with PopoverReader
and frame tags — check out the Tip example:
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.
Unfortunatelly, the workaround doesn't seem to work correctly after device rotation
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)