XUtils

Popovers

A library to present popovers. Simple, modern, and highly customizable. Not boring!


Showroom

Alert Color Menu Tip Standard
Alert Color Menu Tip Standard
Tutorial Picture-in-Picture Notification
Tutorial Picture in Picture Notification

Example

Includes ~20 popover examples. Download

Example app

Customization

🔖 💠 🔲 🟩 🟥 🎾 🛑 👓 👉 🎈 🔰

Customize popovers through the Attributes struct. Pretty much everything is customizable, including positioning, animations, and dismissal behavior.

SwiftUI
Configure in the attributes parameter.
UIKit
Modify the attributes property.

```swift .popover( present: $present, attributes: { $0.position = .absolute( originAnchor: .bottom, popoverAnchor: .topLeft ) } ) { Text("Hi, I'm a popover.") } ```

```swift var popover = Popover { Text("Hi, I'm a popover.") } popover.attributes.position = .absolute( originAnchor: .bottom, popoverAnchor: .topLeft ) present(popover) ```

🔖 Tag • AnyHashable?

Tag popovers to access them later from anywhere. This is useful for updating existing popovers.

/// Set the tag.
$0.tag = "Your Tag"

/// Access it later.
let popover = popover(tagged: "Your Tag") /// Where `self` is a `UIView` or `UIViewController`.

/// If inside a SwiftUI View, use a `WindowReader`:
WindowReader { window in
    let popover = window.popover(tagged: "Your Tag")
}

Note: When you use the .popover(selection:tag:attributes:view:) modifier, this tag is automatically set to what you provide in the parameter.

💠 Position • Position

The popover’s position can either be .absolute (attached to a view) or .relative (picture-in-picture). The enum’s associated value additionally configures which sides and corners are used.

  • Anchors represent sides and corners.
  • For .absolute, provide the origin anchor and popover anchor.
  • For .relative, provide the popover anchors. If there’s multiple, the user will be able to drag between them like a PIP.
Anchor Reference .absolute(originAnchor: .bottom, popoverAnchor: .topLeft) .relative(popoverAnchors: [.right])

⬜ Source Frame • (() -> CGRect)

This is the frame that the popover attaches to or is placed within, depending on its position. This must be in global window coordinates. Because frames are can change so often, this property is a closure. Whenever the device rotates or some other bounds update happens, the closure will be called.

SwiftUI
By default, the source frame is automatically set to the parent view. Setting this will override it.
UIKit
It's highly recommended to provide a source frame, otherwise the popover will appear in the top-left of the screen.

```swift $0.sourceFrame = { /** some CGRect here */ } ```

```swift /// use `weak` to prevent a retain cycle attributes.sourceFrame = { [weak button] in button.windowFrame() } ```

🔲 Source Frame Inset • UIEdgeInsets

Edge insets to apply to the source frame. Positive values inset the frame, negative values expand it.

Absolute Relative
Source view has padding around it, so the popover is offset down. Source view is inset, so the popover is brought more towards the center of the screen.

⏹ Screen Edge Padding • UIEdgeInsets

Global insets for all popovers to prevent them from overflowing off the screen. Kind of like a safe area. Default value is UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16).

🟩 Presentation • Presentation

This property stores the animation and transition that’s applied when the popover appears.

/// Default values:
$0.presentation.animation = .easeInOut
$0.presentation.transition = .opacity

🟥 Dismissal • Dismissal

This property stores the popover’s dismissal behavior. There’s a couple sub-properties here.

/// Same thing as `Presentation`.
$0.dismissal.animation = .easeInOut
$0.dismissal.transition = .opacity

/// Advanced stuff! Here's their default values:
$0.dismissal.mode = .tapOutside
$0.dismissal.tapOutsideIncludesOtherPopovers = false
$0.dismissal.excludedFrames = { [] }
$0.dismissal.dragMovesPopoverOffScreen = true
$0.dismissal.dragDismissalProximity = CGFloat(0.25)

Mode: Configure how the popover should auto-dismiss. You can have multiple at the same time!

  • .tapOutside - dismiss the popover when the user taps outside it.
  • .dragDown - dismiss the popover when the user drags it down.
  • .dragUp - dismiss the popover when the user drags it up.
  • .none - don’t automatically dismiss the popover.

Tap Outside Includes Other Popovers: Only applies when mode is .tapOutside. If this is enabled, the popover will be dismissed when the user taps outside, even when another presented popover is what’s tapped. Normally when you tap another popover that’s presented, the current one will not dismiss.

Excluded Frames: Only applies when mode is .tapOutside. When the user taps outside the popover, but the tap lands on one of these frames, the popover will stay presented. If you want multiple popovers, you should set the source frames of your other popovers as the excluded frames.

/// Set one popover's source frame as the other's excluded frame.
/// This prevents the the current popover from being dismissed before animating to the other one.

let popover1 = Popover { Text("Hello") }
popover1.attributes.sourceFrame = { [weak button1] in button1.windowFrame() }
popover1.attributes.dismissal.excludedFrames = { [weak button2] in [ button2.windowFrame() ] }

let popover2 = Popover { Text("Hello") }
popover2.attributes.sourceFrame = { [weak button2] in button2.windowFrame() }
popover2.attributes.dismissal.excludedFrames = { [weak button1] in [ button1.windowFrame() ] }

Drag Moves Popover Off Screen: Only applies when mode is .dragDown or .dragUp. If this is enabled, the popover will continue moving off the screen after the user drags.

Drag Dismissal Proximity: Only applies when mode is .dragDown or .dragUp. Represents the point on the screen that the drag must reach in order to auto-dismiss. This property is multiplied by the screen’s height.

Diagram with the top 25% of the screen highlighted in blue.

🎾 Rubber Banding Mode • RubberBandingMode

Configures which axes the popover can “rubber-band” on when dragged. The default is [.xAxis, .yAxis].

  • .xAxis - enable rubber banding on the x-axis.
  • .yAxis - enable rubber banding on the y-axis.
  • .none - disable rubber banding.

🛑 Blocks Background Touches • Bool

Set this to true to prevent underlying views from being pressed.

Popover overlaid over some buttons. Tapping on the buttons has no effect.

👓 Accessibility • Accessibilityv1.2.0

Popovers is fully accessible! The Accessibility struct provides additional options for how VoiceOver should read out content.

/// Default values:
$0.accessibility.shiftFocus = true
$0.accessibility.dismissButtonLabel = defaultDismissButtonLabel /// An X icon wrapped in `AnyView?`

Shift Focus: If enabled, VoiceOver will focus the popover as soon as it’s presented.

Dismiss Button Label: A button next to the popover that appears when VoiceOver is on. By default, this is an X circle.

VoiceOver highlights the popover, which has a X button next to id.

Tip: You can also use the accessibility escape gesture (a 2-fingered Z-shape swipe) to dismiss all popovers.

👉 On Tap Outside • (() -> Void)?

A closure that’s called whenever the user taps outside the popover.

🎈 On Dismiss • (() -> Void)?

A closure that’s called when the popover is dismissed.

🔰 On Context Change • ((Context) -> Void)?

A closure that’s called whenever the context changed. The context contains the popover’s attributes, current frame, and other visible traits.


Utilities

📘 🧩 🌃 📖 🏷 📄

Popovers comes with some features to make your life easier.

🧩 Animating Between Popovers

As long as the view structure is the same, you can smoothly transition from one popover to another.

SwiftUI
Use the .popover(selection:tag:attributes:view:) modifier.
UIKit
Get the existing popover using UIResponder.popover(tagged:), then call UIResponder.replace(_:with:).

```swift struct ContentView: View { @State var selection: String? var body: some View { HStack { Button("Present First Popover") { selection = "1" } .popover(selection: $selection, tag: "1") { /// Will be presented when selection == "1". Text("Hi, I'm a popover.") .background(.blue) } Button("Present Second Popover") { selection = "2" } .popover(selection: $selection, tag: "2") { /// Will be presented when selection == "2". Text("Hi, I'm a popover.") .background(.green) } } } } ```

```swift @IBAction func button1Pressed(_ sender: Any) { var newPopover = Popover { Text("Hi, I'm a popover.").background(.blue) } newPopover.attributes.sourceFrame = { [weak button1] in button1.windowFrame() } newPopover.attributes.dismissal.excludedFrames = { [weak button2] in [button2.windowFrame()] } newPopover.attributes.tag = "Popover 1" if let oldPopover = popover(tagged: "Popover 2") { replace(oldPopover, with: newPopover) } else { present(newPopover) /// Present if the old popover doesn't exist. } } @IBAction func button2Pressed(_ sender: Any) { var newPopover = Popover { Text("Hi, I'm a popover.").background(.green) } newPopover.attributes.sourceFrame = { [weak button2] in button2.windowFrame() } newPopover.attributes.dismissal.excludedFrames = { [weak button1] in [button1.windowFrame()] } newPopover.attributes.tag = "Popover 2" if let oldPopover = popover(tagged: "Popover 1") { replace(oldPopover, with: newPopover) } else { present(newPopover) } } ```
Smooth transition between popovers (from blue to green and back.

🌃 Background

You can put anything in a popover’s background.

SwiftUI
Use the .popover(present:attributes:view:background:) modifier.
UIKit
Use the Popover(attributes:view:background:) initializer.

```swift .popover(present: $present) { PopoverView() } background: { /// here! Color.green.opacity(0.5) } ```

```swift var popover = Popover { PopoverView() } background: { /// here! Color.green.opacity(0.5) } ```

Green background over the entire screen, but underneath the popover

📖 Popover Reader

This reads the popover’s context, which contains its frame, window, attributes, and various other properties. It’s kind of like GeometryReader, but cooler. You can put it in the popover’s view or its background.

.popover(present: $present) {
    PopoverView()
} background: {
    PopoverReader { context in
        Path {
            $0.move(to: context.frame.point(at: .bottom))
            $0.addLine(to: context.windowBounds.point(at: .bottom))
        }
        .stroke(Color.blue, lineWidth: 4)
    }
}
Line connects the bottom of the popover with the bottom of the screen

🏷 Frame Tags

Popovers includes a mechanism for tagging and reading SwiftUI view frames. You can use this to provide a popover’s sourceFrame or excludedFrames. Also works great when combined with PopoverReader, for connecting lines with anchor views.

Text("This is a view")
    .frameTag("Your Tag Name") /// Adds a tag inside the window.

/// ...

WindowReader { window in
    Text("Click me!")
    .popover(
        present: $present,
        attributes: {
            $0.sourceFrame = window.frameTagged("Your Tag Name") /// Retrieves a tag from the window.
        }
    )
}

Notes

Popover Hierarchy

Manage a popover’s z-axis level by attaching .zIndex(_:) to its view. A higher index will bring it forwards.

Apps Using Popovers

Find is an app that lets you find text in real life. Popovers is used for the quick tips and as a replacements for menus — download to check it out!

Find App

AnyTracker is an app that tracks numbers, text and prices on websites. It uses Popovers to display sleek dialogs with a nice background blur.

AnyTracker

 

Track Attack! is the ultimate vehicle enthusiast app for the road and track. Modern UI. Dual front & rear cameras. GPS & sensor data. RPM and throttle. Heart rate with Apple Watch. Lap timing for the track & multi-waypoint routing for the road. Easily export and share video with data overlays. Track Attack uses Popovers to display the onboarding tutorial and in-app notifications.

TrackAttack

 

If you have an app that uses Popovers, just make a PR or message me.


Articles

  • coming soon...