# SwiftUI-Popover **Repository Path**: curryluya/SwiftUI-Popover ## Basic Information - **Project Name**: SwiftUI-Popover - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-12 - **Last Updated**: 2026-04-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SwiftUI Popover Lightweight, SwiftUI-only, composable popovers. Attach any custom popover view to any other view deep in your view hierarchy. No UIKit-based hacks. Works across all SwiftUI platforms. > Popovers are rendered exactly where you opt in with `.presentPopovers()`, so they are easy to embed inside sheets, navigation stacks, or feature modules. ### Features - **SwiftUI-only** – Implemented entirely with SwiftUI layout, anchors, and geometry. - **Composable and scoped** – Popovers exist only inside the view subtree where you call `.presentPopovers()`. - **Anchored with arrow** – Arrow follows the effective attachment point of the popover. - **Cross-platform** – Designed to run on iOS, macOS, watchOS, tvOS, and visionOS. - **Configurable dismissal** – Optional dimming and tap/drag-to-dismiss behavior. - **Multiple popovers** – Support for multiple popovers, with an option to make a popover "exclusive".
--- ![SwiftUI-Popover preview across platforms](Assets/all-platforms.png) ## Requirements - Swift 6 (Swift tools version 6.0) or newer - Xcode 16 or newer - Platforms: - iOS 16+ - macOS 14+ - watchOS 11+ - tvOS 18+ - visionOS 1+ Adjust the platforms in `Package.swift` as needed. --- ## Installation ### Swift Package Manager (Xcode) 1. In Xcode, open your project. 2. Go to **File → Add Packages…** 3. Enter the repository URL, for example: ``` https://github.com/qusc/SwiftUI-Popover.git ``` 4. Add the **SwiftUI-Popover** product to your app target. Then: ```swift import SwiftUI import SwiftUI_Popover ``` The Swift Package product is named `SwiftUI-Popover`, and the module you import in Swift code is `SwiftUI_Popover`. --- ## Basic usage Wrap the part of your view hierarchy that should be able to show popovers in `.presentPopovers()`, and attach popovers with `.swiftUIPopover`. ```swift import SwiftUI import SwiftUI_Popover struct ContentView: View { @State private var isPopoverPresented = true @GestureState private var dragOffset: CGSize = .zero var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) // Attach the popover to this view .swiftUIPopover( isPresented: $isPopoverPresented, preferredAttachmentEdge: .top ) { PopoverMessageBubble(fill: Color.blue) { Text("SwiftUI-only popover attached to\n`Image(systemName: \"globe\")`.") .foregroundStyle(.white) #if os(watchOS) .font(.system(size: 14)) #endif } .shadow(radius: 20) } Text("Hello, world!") } // Optional: draggable demo with a spring snap-back .offset(x: dragOffset.width, y: dragOffset.height) .animation( .spring(response: 0.35, dampingFraction: 0.7, blendDuration: 0), value: dragOffset ) #if !os(tvOS) .gesture( DragGesture() .updating($dragOffset) { value, state, _ in state = value.translation } ) #endif .frame(maxWidth: .infinity, maxHeight: .infinity) // Install the popover presentation layer for this subtree .presentPopovers() #if !os(watchOS) .padding(4) #endif } } ``` See the example app in the `Example/` directory. ## Real-world example A short clip from a production app using `SwiftUI-Popover`: ## Toolbar menu example You can also use `SwiftUI-Popover` to build lightweight toolbar menus – including on watchOS, where SwiftUI's built-in `Menu` is not available. Example toolbar integration: ```swift import SwiftUI_Popover import SwiftUI struct ToolbarMenuExample: View { @State private var showToolbarMenu = false var body: some View { NavigationStack { Text("Toolbar menu example") .navigationTitle("Inbox") .toolbar { #if os(watchOS) let placement: ToolbarItemPlacement = .topBarTrailing #else let placement: ToolbarItemPlacement = .automatic #endif ToolbarItem(placement: placement) { Button { withAnimation { showToolbarMenu = true } } label: { Image(systemName: "ellipsis") } .swiftUIPopover( isPresented: $showToolbarMenu, disableDelay: true, isExclusive: true, isDismissible: true ) { PopoverMessageBubble(fill: .clear, enableGlassEffect: true) { VStack(alignment: .leading) { Group { Button("Lightning", systemImage: "bolt.fill", action: { }) Button("Sleep", systemImage: "moon.fill", action: { }) } .padding() } } } } } } .presentPopovers() } } ``` The included example project also features a toolbar menu and demonstrates the coordination of multiple popovers. --- ## API overview ### 1. Attaching a popover ```swift swiftUIPopover( isPresented: Binding, disableDelay: Bool = false, isExclusive: Bool = false, isDismissible: Bool = false, preferredAttachmentEdge: VerticalEdge? = nil, @ViewBuilder content: () -> some View ) ``` - `isPresented` – Controls visibility of this popover. - `disableDelay` – Show immediately instead of delaying the initial appearance (if you want). - `isExclusive` – If `true`, this popover hides others while it is visible. - `isDismissible` – If `true`, a dimming background is added and tapping / dragging it dismisses the popover. - `preferredAttachmentEdge` – Force the popover to attach to `.top` or `.bottom`. If `nil`, the library picks an edge based on available space. Example: ```swift Button("Show details") { isPopoverPresented.toggle() } .swiftUIPopover( isPresented: $isPopoverPresented, isDismissible: true, isExclusive: true, preferredAttachmentEdge: .top ) { PopoverMessageBubble(fill: Color(.systemBackground)) { Text("Details about this button.") } } ``` ### 2. Installing the presentation layer ```swift presentPopovers(padding: EdgeInsets? = nil) ``` Call this once on the container where you want popovers to be rendered. This prevents popovers from propagating to other calls of `presentPopovers(padding:)` at higher levels in the view hierarchy. ```swift VStack { // content that uses `.swiftUIPopover` } .presentPopovers() ``` Popovers are drawn in a single overlay relative to this container, not globally at the window level. --- ## `PopoverMessageBubble` `PopoverMessageBubble` is generic over two `ShapeStyle` types for its fills: ```swift public struct PopoverMessageBubble: View { ... } ``` `PopoverMessageBubble` is a reusable bubble + arrow container for your popover content. Conceptually: - Renders a rounded rectangle with an arrow. - Positions the arrow along the edge based on an effective attachment point. - Applies platform-specific padding so content looks right on watchOS and other platforms. Public initializer: ```swift public init( showArrow: Bool = true, fill: F, secondaryFill: S? = nil, enableGlassEffect: Bool = false, padding: EdgeInsets = PopoverConstants.defaultPadding, @ViewBuilder content: @escaping () -> Content ) ``` Parameters: - `showArrow` – Whether to draw the arrow or render a plain rounded rectangle. - `fill` – Primary background `ShapeStyle` for the bubble. - `secondaryFill` – Optional secondary `ShapeStyle` layered on top (e.g. to combine `Material` with a `Color`). - `enableGlassEffect` – Enables an optional glassy material effect inside the bubble. - `padding` – Insets applied to the popover content inside the bubble. Typical usage: ```swift PopoverMessageBubble(fill: Color.blue) { VStack(alignment: .leading, spacing: 4) { Text("Popover title") .font(.headline) Text("Some additional information goes here.") .font(.subheadline) } .foregroundStyle(.white) } .shadow(radius: 16) ``` You can also control padding and secondary fill (e.g. to combine `Material.regular` with another semi-transparent `Color`). --- ## Why not just use SwiftUI’s built-in .popover modifier? The built-in `.popover` modifier is great on iPadOS and macOS, but: - Popovers are not available on watchOS. - On iPhone (compact width), `.popover` often adapts into a sheet or full-screen presentation instead of staying as an anchored bubble. - The presentation is global and harder to scope to a specific subtree or feature module. - Arrow placement and appearance are controlled by the system. This library focuses on: - A small, composable primitive for anchored popovers. - Predictable behavior across platforms and size classes. - Full control over layout and styling in pure SwiftUI. --- ## How it works Implementation details, in case you are curious: - Uses `Anchor` and a `PreferenceKey` to collect popover anchors from the view hierarchy. - The `presentPopovers` modifier reads all popover preferences and renders them in a single overlay. - `onGeometryChange` is used to measure content size, which is combined with: - The anchor rect of the source view. - The container frame. - The library computes an ideal frame for the popover, clamps it to the available bounds, and derives an effective attachment point used to position the arrow. - The arrow is a custom SwiftUI `Shape` that tracks this attachment point along the edge. --- ## Example project The repository contains an example app in the `Example/` directory that demonstrates: - Basic anchored popovers attached to a symbol. - Draggable content view (on non-tvOS platforms) to visualize how the arrow tracks the attachment point. - watchOS support and tvOS-specific behavior (tap-to-dismiss instead of drag). Open the example Xcode project, select a target (iOS, watchOS, etc.), and run. --- ## License This project is licensed under the MIT License. See the [`LICENSE`](LICENSE) file for details.