Presenting Popovers in SwiftUI

Popover is a system-provided control used widely in applications running both in iPad and Mac. It allows to display additional content, or ask for user interaction, in a concise and elegant fashion; most importantly, users are familiar with it, so it is easy for them to use popovers.

Presenting a popover in SwiftUI is quite similar to the way alerts are presented. That means that there is a specific view modifier to call in order to display it, as well as to provide a few configuration options. Note that popovers show up as alerts in iPhone; they are meant for iPad and Mac devices only.

Presenting a popover

Suppose that we have the following simple button implementation in SwiftUI:

VStack {
    Button {
        
    } label: {
        Text("Today")
            .font(.title2)
            .foregroundColor(.white)
    }
}
.frame(minWidth: 400, minHeight: 250)
.background(Color.indigo)
.cornerRadius(8)

The goal here is to display a popover when the button gets tapped in order to show the current date. Popover is not a view that we just need to initialize and provide arguments. Instead, there is a view modifier responsible to present it. And similarly to alerts, we trigger its apppearance by toggling the value of a @State boolean property, just like the next one:

@State private var showPopover = false

The initial false value indicates that the popover is initially hidden. To change that, we just make the above true in the button's action closure:

Button {
    showPopover = true
} label: {
    ...
}

In the actual topic now, we present a popover with the following view modifier. See that the first argument is the binding value of the showPopover property:

VStack {
    ...
}
.popover(isPresented: $showPopover) {
    
}

The second argument of the popover modifier is the content closure; the place where we add the content that we want to show in the popover. This can be anything; a series of SwiftUI views implemented locally in the closure, or a call to another custom view that implements the content:

.popover(isPresented: $showPopover) {
    PopoverContent()
}

In this particular example, PopoverContent is a another custom SwiftUI view. Its purpose is to simply present a title along with the current date like so:

struct PopoverContent: View {
    var formattedDate: String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .full
        return dateFormatter.string(from: Date())
    }
    
    var body: some View {
        VStack {
            Text("Today is:")
                .font(.title)
                .fontWeight(.bold)
                .padding(.bottom, 20)
            
            Text(formattedDate)
                .font(.title2)
        }
        .frame(width: 350, height: 200)   
    }
}

Keep something important in mind here. The size we set to the VStack is going to be the popover's size as well. So, make sure to always set a frame in the contained view.

Specifying popover's position and direction

Running the implementation we have so far, this is what we'll see on an iPad:

A popover pointing from towards bottom to the container view of the button on iPad.

The first observation we can make regards the position of the popover. See that it's pointing to the container view of the button, instead of pointing to the button that triggers its appearance. This is happening for one simple reason; the popover modifier is not applied to the proper view.

Fixing that issue is easy; we have to move the popover modifier from the VStack that is currently applied to the button:

Button {
    ...
} label: {
    ...
}
.popover(isPresented: $showPopover) {
    PopoverContent()
}

The second thing to notice is the popover's pointing direction and arrow edge. The system automatically placed popover above the button, pointing towards bottom. However, what if the default appearance is not what we want? What if we need to place the popover in a different position, and therefore have it pointing to a different direction?

The answer to that comes through two additional arguments that we can optionally pass to the popover's modifier. The first one is the attachementAnchor. Practically speaking, with it we can specify the anchor point of the view that the popover will point to; top, bottom, leading, trailing, top-leading, top-trailing, and so on.

However, setting just the anchor point is usually not enough. It's also necessary to specify a matching edge to the popover's arrow. Otherwise, popover's appearance will most probably be wrong.

So, the other argument to provide is the arrowEdge, passing as value the wanted arrow edge.

In code, the following example shows how we can present the popover on the trailing edge of the button:

.popover(isPresented: $showPopover,
         attachmentAnchor: .point(.trailing),
         arrowEdge: .trailing) {
    ...
}

Unfortunately, there is an actual issue here:

The arrowEdge value seems not to be working on iPadOS, with the system totally ignoring it.

On macOS, however, the proper combination of the attachmentAnchor and arrowEdge have the desired effect:

A popover pointing to the trailing edge of the button on macOS.

Conclusion

So, that's all you need to know in order to present a popover in SwiftUI. Summarizing what's really important to remember, the view to apply the popover modifier matters, as this is where popover points to. Secondly, specifying a size is necessary, as the system has no way to know how big or small we want it to be. And finally, use the additional arguments demonstrated above in order to change the anchor point of the view that the popover points to, and the arrow edge. If you're working on a macOS app, then the last two will work without any issues, even if iPadOS seems to disregard them and position the popover automatically. Thank you for reading!