SwiftUI: Change Popover Arrow Color

Viewed 931

In SwiftUI, how does one change the color of the arrow that connects a popover to its anchor point?

When working with the underlying UIPopoverController outside of SwiftUI, I believe it's done by changing the backgroundColor property, but I don't see a way to access that here. Even setting background as the very last modifier only changes the view within the popover; not the popover itself.

For example, adding the following code to a view:

@State private var showDetailedView: Bool = false

// ...

.popover(isPresented: self.$showDetailedView) {
    Text("Hello!")
        .padding()
        .background(Color.red)
}
.onTapGesture {
    self.showDetailedView = true
}

...results in an arrow that's still the default background color (this example taken from native macOS in Dark Mode):

Example popover on macOS, with default arrow color

...and like this on iOS (running via Catalyst), which is even worse!

Example popover on iOS Catalyst, with default arrow color

1 Answers

Here is a pure SwiftUI solution using GeometryReader and two .frame calls. The key idea is to make a background larger than the size of your presented view. Since SwiftUI does not clip contents at this moment, this will override the default background on the popover arrow.

Do notice that this only works with a solid background. In Catalyst, a solid background is already painted so transparent content would reveal the ugly black as you have posted. We might have to resort to things like UIViewRepresentable for such case.

Consider the following example that changes the color of an arrow on the top edge:

.background(GeometryReader { geometry in
    Color
        .white
        .frame(width: geometry.size.width,
                height: geometry.size.height + 100)
        .frame(width: geometry.size.width,
                height: geometry.size.height,
                alignment: .bottom)
})

Explanation:

  • The first inner frame creates a white rectangle that is 100px higher than your presented view.
  • The second outer frame creates a new frame that is of the same size as your presented view. this is achieved through the GeometryReader.
  • The alignment: argument in the second outer frame makes sure that these two frames align on the bottom.
  • Since the outer frame is as large as the GeometryReader, it fills the whole background of your presented view.

The "overflowed" content overrides the default black arrow color.

To make this work with arbitrary arrow edge, you might want to change the inner frame, increasing both the width and height. As for the alignment for outer frame, using the default argument of .center should work.

Related