Skip to content

How to use Popover modifier in SwiftUI | SwiftUI Bootcamp #69

A popover is a small floating panel that appears anchored to the button that triggered it — perfect for context-sensitive options, quick feedback forms, or any secondary interaction that doesn't need a full-screen sheet. On iPhone, .presentationCompactAdaptation(.popover) ensures it behaves like a true popover instead of adapting to a sheet.

What You'll Learn

  • How to present a popover using the .popover(isPresented:attachmentAnchor:content:) modifier
  • How attachmentAnchor controls where the popover arrow points
  • How .presentationCompactAdaptation(.popover) keeps the popover style on compact (iPhone) screens

Mental Model

Think of a popover like a speech bubble in a comic. The bubble contains text (your content), and there's an arrow pointing to exactly who is speaking (the anchor). On iPad, popovers naturally look like speech bubbles attached to the button that triggered them. On iPhone — which has a compact screen — SwiftUI normally adapts popovers to sheets because sheets feel more natural there. .presentationCompactAdaptation(.popover) says "no, keep the speech bubble, even on small screens."

Detailed Explanation

.popover(isPresented:attachmentAnchor:arrowEdge:content:) presents a floating panel when the isPresented binding becomes true. Unlike a sheet, a popover is dismissed by tapping outside it (no swipe gesture). The popover is logically attached to the view it's applied to.

attachmentAnchor determines where the popover arrow appears. .point(.top) anchors the arrow to the top center of the trigger button — the popover appears above it. .point(.bottom) puts the arrow at the bottom — the popover appears below. The available UnitPoint values include .top, .bottom, .leading, .trailing, .topLeading, etc.

On iPhone (compact horizontal size class), SwiftUI historically adapted .popover to a full-screen sheet because popovers don't fit well on small screens. .presentationCompactAdaptation(.popover) — available since iOS 16.4 — overrides this adaptation and forces true popover behavior even on iPhone. Use this when your popover content is genuinely small and contextual, not when it contains a lot of information that would be better in a sheet.

The content closure of the popover is a regular SwiftUI view. Here a ScrollView containing a VStack of feedback options demonstrates a typical pattern: a small menu of choices that the user taps once and the popover dismisses.

Code Structure

NativePopoverBootcamp.swift contains a feedback button positioned at the bottom of the screen. Tapping it reveals a popover anchored above the button with three feedback options. The list of options is in a @State array, making it easy to add, remove, or reorder items. The dividers between options are conditionally rendered to avoid a trailing separator.

Complete Code

NativePopoverBootcamp.swift

swift
import SwiftUI

struct NativePopoverBootcamp: View {
    
    @State private var showPopover: Bool = false
    @State private var feedbackOptions: [String] = [
        "Very good 🥳",
        "Average 🙂",
        "Very bad 😡"
    ]
    
    var body: some View {
        ZStack {
            Color.gray.ignoresSafeArea() // Background to visually contrast the popover
            
            VStack {
                
                Spacer() // Pushes the button to the bottom of the screen
                
                // The popover modifier is applied to the button — the popover anchors to this view
                Button("Provide feedback?") {
                    showPopover.toggle()
                }
                .padding(20)
                .background(Color.yellow)
                .popover(isPresented: $showPopover, attachmentAnchor: .point(.top), content: {
                    // Popover appears above the button because the attachment point is .top
                    ScrollView {
                        VStack(alignment: .leading, spacing: 12, content: {
                            ForEach(feedbackOptions, id: \.self) { option in
                                Button(option) {
                                    // Selecting an option would go here — e.g., dismiss and record feedback
                                }
                                
                                // Adds a divider between options but not after the last one
                                if option != feedbackOptions.last {
                                    Divider()
                                }
                            }
                        })
                        .padding(20)
                    }
                    // Forces popover style on iPhone instead of adapting to a sheet
                    .presentationCompactAdaptation(.popover)
                })
            }
        }
    }
}

struct NativePopoverBootcamp_Previews: PreviewProvider {
    static var previews: some View {
        NativePopoverBootcamp()
    }
}

Code Walkthrough

  1. @State private var showPopover: Bool = false — The Boolean that drives the popover's visibility. Setting it to true presents the popover; the system sets it back to false when the user taps outside.

  2. @State private var feedbackOptions: [String] — The options array is @State so it could be modified at runtime (adding, removing options). Using an array also lets you use ForEach with a single source of truth rather than hard-coding each button.

  3. .popover(isPresented: $showPopover, attachmentAnchor: .point(.top)) — Applied directly to the Button. This means the popover arrow points to this button specifically. attachmentAnchor: .point(.top) puts the arrow at the top of the button, so the popover floats above it.

  4. ScrollView inside the popover — The popover content can be any view hierarchy. ScrollView is used here so that if the options list grew long, users could still access all options. The VStack inside has .leading alignment for a clean left-aligned list.

  5. if option != feedbackOptions.last { Divider() } — This idiom renders dividers between all items except after the last one. This is a common SwiftUI pattern for inter-item separators without a trailing separator. Note: comparing with .last works because strings are Equatable.

  6. .presentationCompactAdaptation(.popover) — Applied on the popover's content view (not on the presenter). This is the modern iOS 16.4+ way to keep popover style on iPhone. Without it, the popover becomes a sheet on compact screens.

Common Mistakes

Mistake: Applying .presentationCompactAdaptation to the presenting view instead of the content view
.presentationCompactAdaptation(.popover) must be placed on the view inside the popover closure, not on the button that shows it. It's a presentation modifier like .presentationDetents — it belongs on the presented content.

Mistake: Using .popover for large content on iPhone
Even with .presentationCompactAdaptation(.popover), a popover with lots of content is hard to use on iPhone's small screen. If your content has more than 5–6 items or needs significant height, present a .sheet with .presentationDetents([.medium]) instead — it's more thumb-friendly.

Mistake: Attaching the .popover modifier to a container instead of the trigger control
If you attach .popover to a VStack instead of the specific Button inside it, the popover's arrow points to the VStack's frame — not the button. Always attach .popover to the exact view you want the arrow to point at.

Key Takeaways

  • .popover creates a floating panel anchored to the view it's applied to; attachmentAnchor controls where the arrow points
  • On iPhone, SwiftUI adapts popovers to sheets by default — use .presentationCompactAdaptation(.popover) (iOS 16.4+) to force true popover behavior
  • Tap outside the popover to dismiss it — there's no swipe gesture or dismiss button needed

Last updated: June 27, 2026

Released under the MIT License.