{"id":2233,"url":"https://github.com/aheze/Popovers","last_synced_at":"2025-08-06T15:31:01.856Z","repository":{"id":40779309,"uuid":"441005020","full_name":"aheze/Popovers","owner":"aheze","description":"A library to present popovers. Simple, modern, and highly customizable. Not boring!","archived":false,"fork":false,"pushed_at":"2024-06-26T04:57:10.000Z","size":50107,"stargazers_count":1984,"open_issues_count":65,"forks_count":113,"subscribers_count":10,"default_branch":"main","last_synced_at":"2024-12-05T01:08:05.872Z","etag":null,"topics":["alert","ios","notification","popover","popup","snackbar","swift","swiftui","uiwindow"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aheze.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2021-12-22T23:02:04.000Z","updated_at":"2024-12-02T12:21:43.000Z","dependencies_parsed_at":"2023-02-17T03:00:55.225Z","dependency_job_id":"c0896827-3035-4760-a309-de058366398d","html_url":"https://github.com/aheze/Popovers","commit_stats":{"total_commits":290,"total_committers":8,"mean_commits":36.25,"dds":"0.12068965517241381","last_synced_commit":"0269cf9cb7dbdea58bb4f24987efd06443687322"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aheze%2FPopovers","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aheze%2FPopovers/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aheze%2FPopovers/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aheze%2FPopovers/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aheze","download_url":"https://codeload.github.com/aheze/Popovers/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228915470,"owners_count":17991410,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alert","ios","notification","popover","popup","snackbar","swift","swiftui","uiwindow"],"created_at":"2024-01-05T20:16:08.379Z","updated_at":"2024-12-09T15:30:53.842Z","avatar_url":"https://github.com/aheze.png","language":"Swift","funding_links":[],"categories":["Libs","Swift","UI","Uncategorized","Swift UI"],"sub_categories":["UI","Font","Other Testing","Uncategorized"],"readme":"![Header Image](Assets/Header.png)\n\n# Popovers\nA library to present popovers.\n\n- Present **any** view above your app's main content.\n- Attach to source views or use picture-in-picture positioning.\n- Display multiple popovers at the same time with smooth transitions.\n- Supports SwiftUI, UIKit, and multitasking windows on iPadOS.\n- Highly customizable API that's super simple — just add `.popover`.\n- Drop-in replacement for iOS 14's `Menu` that works on iOS 13.\n- SwiftUI-based core for a lightweight structure. 0 dependencies.\n- It's 2023 — about time that popovers got interesting!\n\n## Showroom\n\n\n\u003ctable\u003e\n\n\u003ctr\u003e\n\u003ctd\u003e\nAlert   \n\u003c/td\u003e\n\u003ctd\u003e\nColor  \n\u003c/td\u003e\n\u003ctd\u003e\nMenu   \n\u003c/td\u003e\n\u003ctd\u003e\nTip      \n\u003c/td\u003e\n\u003ctd\u003e\nStandard \n\u003c/td\u003e\n\u003c/tr\u003e\n\n\u003ctr\u003e\n\u003c/tr\u003e\n  \n\u003ctr\u003e\n\u003ctd\u003e\n\u003cimg src=\"Assets/GIFs/Alert.gif\" alt=\"Alert\"\u003e\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cimg src=\"Assets/GIFs/Color.gif\" alt=\"Color\"\u003e\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cimg src=\"Assets/GIFs/Menu.gif\" alt=\"Menu\"\u003e\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cimg src=\"Assets/GIFs/Tip.gif\" alt=\"Tip\"\u003e\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cimg src=\"Assets/GIFs/Standard.gif\" alt=\"Standard\"\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\n\u003ctr\u003e\n\u003c/tr\u003e\n  \n\u003ctr\u003e\n\u003ctd colspan=2\u003e\nTutorial\n\u003c/td\u003e\n\u003ctd colspan=2\u003e\nPicture-in-Picture\n\u003c/td\u003e\n\u003ctd\u003e\nNotification\n\u003c/td\u003e\n\u003c/tr\u003e\n  \n\u003ctr\u003e\n\u003c/tr\u003e\n  \n\u003ctr\u003e\n\u003ctd colspan=2\u003e\n\u003cimg src=\"Assets/GIFs/Tutorial.gif\" alt=\"Tutorial\"\u003e\n\u003c/td\u003e\n\u003ctd colspan=2\u003e\n\u003cimg src=\"Assets/GIFs/PIP.gif\" alt=\"Picture in Picture\"\u003e\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cimg src=\"Assets/GIFs/Notification.gif\" alt=\"Notification\"\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\n\u003c/table\u003e\n\n## Example\nIncludes ~20 popover examples. [Download](https://github.com/aheze/Popovers/tree/main/Examples)\n\n![Example app](Assets/ExampleApp.png)\n\n## Installation\nRequires iOS 13+. Popovers can be installed through the [Swift Package Manager](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app) (recommended) or [Cocoapods](https://cocoapods.org/).\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nSwift Package Manager\n\u003c/strong\u003e\n\u003cbr\u003e\nAdd the Package URL:\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nCocoapods\n\u003c/strong\u003e\n\u003cbr\u003e\nAdd this to your Podfile:\n\u003c/td\u003e\n\u003c/tr\u003e\n  \n\u003ctr\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```\nhttps://github.com/aheze/Popovers\n```\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```\npod 'Popovers'\n```\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n\n\n## Usage\n\nTo present a popover in SwiftUI, use the `.popover(present:attributes:view)` modifier. By default, the popover uses its parent view as the source frame.\n\n```swift\nimport SwiftUI\nimport Popovers\n\nstruct ContentView: View {\n    @State var present = false\n    \n    var body: some View {\n        Button(\"Present popover!\") {\n            present = true\n        }\n        .popover(present: $present) { /// here!\n            Text(\"Hi, I'm a popover.\")\n                .padding()\n                .foregroundColor(.white)\n                .background(.blue)\n                .cornerRadius(16)\n        }\n    }\n}\n```\n\nIn UIKit, create a `Popover` instance, then present with `UIViewController.present(_:)`. You should also set the source frame.\n\n```swift\nimport SwiftUI\nimport Popovers\n\nclass ViewController: UIViewController {\n    @IBOutlet weak var button: UIButton!\n    @IBAction func buttonPressed(_ sender: Any) {\n        var popover = Popover { PopoverView() }\n        popover.attributes.sourceFrame = { [weak button] in\n            button.windowFrame()\n        }\n        \n        present(popover) /// here!\n    }\n}\n\nstruct PopoverView: View {\n    var body: some View {\n        Text(\"Hi, I'm a popover.\")\n            .padding()\n            .foregroundColor(.white)\n            .background(.blue)\n            .cornerRadius(16)\n    }\n}\n```\n\n\u003cimg src=\"Assets/UsagePopover.png\" width=300 alt=\"Button 'Present popover!' with a popover underneath.\"\u003e\n\n\u003cbr\u003e\n\n## Customization\n| [🔖](https://github.com/aheze/Popovers#tag--string)  | [💠](https://github.com/aheze/Popovers#position--position)  | [⬜](https://github.com/aheze/Popovers#source-frame-----cgrect)  | [🔲](https://github.com/aheze/Popovers#source-frame-inset--uiedgeinsets)  | [⏹](https://github.com/aheze/Popovers#screen-edge-padding--uiedgeinsets)  | [🟩](https://github.com/aheze/Popovers#presentation--presentation)  | [🟥](https://github.com/aheze/Popovers#dismissal--dismissal)  | [🎾](https://github.com/aheze/Popovers#rubber-banding-mode--rubberbandingmode)  | [🛑](https://github.com/aheze/Popovers#blocks-background-touches--bool)  | [👓](https://github.com/aheze/Popovers#accessibility--accessibility--v120)  | [👉](https://github.com/aheze/Popovers#on-tap-outside-----void)  | [🎈](https://github.com/aheze/Popovers#on-dismiss-----void)  | [🔰](https://github.com/aheze/Popovers#on-context-change--context---void)  |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n\nCustomize popovers through the `Attributes` struct. Pretty much everything is customizable, including positioning, animations, and dismissal behavior.\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nSwiftUI\n\u003c/strong\u003e\n\u003cbr\u003e\nConfigure in the \u003ccode\u003eattributes\u003c/code\u003e parameter.\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nUIKit\n\u003c/strong\u003e\n\u003cbr\u003e\nModify the \u003ccode\u003eattributes\u003c/code\u003e property.\n\u003c/td\u003e\n\u003c/tr\u003e\n  \n\u003ctr\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```swift\n.popover(\n    present: $present,\n    attributes: {\n        $0.position = .absolute(\n            originAnchor: .bottom,\n            popoverAnchor: .topLeft\n        )\n    }\n) {\n    Text(\"Hi, I'm a popover.\")\n}\n```\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```swift\nvar popover = Popover {\n    Text(\"Hi, I'm a popover.\")\n}\n\npopover.attributes.position = .absolute(\n    originAnchor: .bottom,\n    popoverAnchor: .topLeft\n)\n\npresent(popover)\n```\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n### 🔖 Tag • `AnyHashable?`\nTag popovers to access them later from anywhere. This is useful for updating existing popovers.\n\n```swift\n/// Set the tag.\n$0.tag = \"Your Tag\"\n\n/// Access it later.\nlet popover = popover(tagged: \"Your Tag\") /// Where `self` is a `UIView` or `UIViewController`.\n\n/// If inside a SwiftUI View, use a `WindowReader`:\nWindowReader { window in\n    let popover = window.popover(tagged: \"Your Tag\")\n}\n```\n\n**Note:** When you use the `.popover(selection:tag:attributes:view:)` modifier, this `tag` is automatically set to what you provide in the parameter.\n\n### 💠 Position • `Position`\nThe 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.\n\n- `Anchor`s represent sides and corners.\n- For `.absolute`, provide the origin anchor and popover anchor.\n- For `.relative`, provide the popover anchors. If there's multiple, the user will be able to drag between them like a PIP.\n\nAnchor Reference | `.absolute(originAnchor: .bottom, popoverAnchor: .topLeft)` | `.relative(popoverAnchors: [.right])`\n--- | --- | ---\n![](Assets/Anchors.png) | ![](Assets/Absolute.png) | ![](Assets/Relative.png)\n\n### ⬜ Source Frame • `(() -\u003e CGRect)`\nThis 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.\n\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nSwiftUI\n\u003c/strong\u003e\n\u003cbr\u003e\nBy default, the source frame is automatically set to the parent view. Setting this will override it.\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nUIKit\n\u003c/strong\u003e\n\u003cbr\u003e\nIt's highly recommended to provide a source frame, otherwise the popover will appear in the top-left of the screen.\n\u003c/td\u003e\n\u003c/tr\u003e\n  \n\u003ctr\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```swift\n$0.sourceFrame = {\n    /** some CGRect here */\n}\n```\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```swift\n /// use `weak` to prevent a retain cycle\nattributes.sourceFrame = { [weak button] in\n    button.windowFrame()\n}\n```\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n### 🔲 Source Frame Inset • `UIEdgeInsets`\nEdge insets to apply to the source frame. Positive values inset the frame, negative values expand it.\n\nAbsolute | Relative\n--- | ---\n![Source view has padding around it, so the popover is offset down.](Assets/SourceFrameInsetAbsolute.png) | ![Source view is inset, so the popover is brought more towards the center of the screen.](Assets/SourceFrameInsetRelative.png)\n\n### ⏹ Screen Edge Padding • `UIEdgeInsets`\nGlobal 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)`.\n\n### 🟩 Presentation • `Presentation`\nThis property stores the animation and transition that's applied when the popover appears.\n\n```swift\n/// Default values:\n$0.presentation.animation = .easeInOut\n$0.presentation.transition = .opacity\n```\n\n### 🟥 Dismissal • `Dismissal`\nThis property stores the popover's dismissal behavior. There's a couple sub-properties here.\n\n```swift\n/// Same thing as `Presentation`.\n$0.dismissal.animation = .easeInOut\n$0.dismissal.transition = .opacity\n\n/// Advanced stuff! Here's their default values:\n$0.dismissal.mode = .tapOutside\n$0.dismissal.tapOutsideIncludesOtherPopovers = false\n$0.dismissal.excludedFrames = { [] }\n$0.dismissal.dragMovesPopoverOffScreen = true\n$0.dismissal.dragDismissalProximity = CGFloat(0.25)\n```\n\n**Mode:** Configure how the popover should auto-dismiss. You can have multiple at the same time!\n- `.tapOutside` - dismiss the popover when the user taps outside it.\n- `.dragDown` - dismiss the popover when the user drags it down.\n- `.dragUp` - dismiss the popover when the user drags it up.\n- `.none` - don't automatically dismiss the popover.\n\n**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.\n\n**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.\n\n```swift\n/// Set one popover's source frame as the other's excluded frame.\n/// This prevents the the current popover from being dismissed before animating to the other one.\n\nlet popover1 = Popover { Text(\"Hello\") }\npopover1.attributes.sourceFrame = { [weak button1] in button1.windowFrame() }\npopover1.attributes.dismissal.excludedFrames = { [weak button2] in [ button2.windowFrame() ] }\n\nlet popover2 = Popover { Text(\"Hello\") }\npopover2.attributes.sourceFrame = { [weak button2] in button2.windowFrame() }\npopover2.attributes.dismissal.excludedFrames = { [weak button1] in [ button1.windowFrame() ] }\n```\n\n**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.\n\n**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.\n\n\n\u003cimg src=\"Assets/DragDismissalProximity.png\" width=300 alt=\"Diagram with the top 25% of the screen highlighted in blue.\"\u003e\n\n\n### 🎾 Rubber Banding Mode • `RubberBandingMode`\nConfigures which axes the popover can \"rubber-band\" on when dragged. The default is `[.xAxis, .yAxis]`.\n\n- `.xAxis` - enable rubber banding on the x-axis.\n- `.yAxis` - enable rubber banding on the y-axis.\n- `.none` - disable rubber banding.\n\n### 🛑 Blocks Background Touches • `Bool`\nSet this to true to prevent underlying views from being pressed.\n\n\u003cimg src=\"Assets/BlocksBackgroundTouches.png\" width=300 alt=\"Popover overlaid over some buttons. Tapping on the buttons has no effect.\"\u003e\n\n### 👓 Accessibility • `Accessibility` • [*`v1.2.0`*](https://github.com/aheze/Popovers/releases/tag/1.2.0)\nPopovers is fully accessible! The `Accessibility` struct provides additional options for how VoiceOver should read out content.\n\n```swift\n/// Default values:\n$0.accessibility.shiftFocus = true\n$0.accessibility.dismissButtonLabel = defaultDismissButtonLabel /// An X icon wrapped in `AnyView?`\n```\n**Shift Focus:** If enabled, VoiceOver will focus the popover as soon as it's presented.\n\n**Dismiss Button Label:** A button next to the popover that appears when VoiceOver is on. By default, this is an \u003ckbd\u003eX\u003c/kbd\u003e circle.\n\n| \u003cimg src=\"Assets/Accessibility.png\" width=300 alt=\"VoiceOver highlights the popover, which has a X button next to id.\"\u003e |\n| --- |\n\nTip: You can also use the accessibility escape gesture (a 2-fingered Z-shape swipe) to dismiss all popovers.\n\n### 👉 On Tap Outside • `(() -\u003e Void)?`\nA closure that's called whenever the user taps outside the popover.\n\n### 🎈 On Dismiss • `(() -\u003e Void)?`\nA closure that's called when the popover is dismissed.\n\n### 🔰 On Context Change • `((Context) -\u003e Void)?`\nA closure that's called whenever the context changed. The context contains the popover's attributes, current frame, and other visible traits.\n\n\u003cbr\u003e\n\n## Utilities\n| [📘](https://github.com/aheze/Popovers#menus)  | [🧩](https://github.com/aheze/Popovers#animating-between-popovers)  | [🌃](https://github.com/aheze/Popovers#background)  | [📖](https://github.com/aheze/Popovers#popover-reader)  | [🏷](https://github.com/aheze/Popovers#frame-tags)  | [📄](https://github.com/aheze/Popovers#templates)  |\n| --- | --- | --- | --- | --- | --- |\n\nPopovers comes with some features to make your life easier.\n\n### 📘 Menus\nNew in [v1.3.0](https://github.com/aheze/Popovers/releases/tag/1.3.0)! The template `Menu` looks and behaves pretty much exactly like the system menu, but also works on iOS 13. It's also extremely customizable with support for manual presentation and custom views.\n\n| \u003cimg src=\"Assets/MenuComparison.gif\" width=500 alt=\"The system menu and Popovers' custom menu, side by side\"\u003e |\n| --- |\n\n\u003cdetails\u003e\n\u003csummary\u003eSwiftUI (Basic)\u003c/summary\u003e\n\n```swift\nstruct ContentView: View {\n    var body: some View {\n        Templates.Menu {\n            Templates.MenuButton(title: \"Button 1\", systemImage: \"1.circle.fill\") { print(\"Button 1 pressed\") }\n            Templates.MenuButton(title: \"Button 2\", systemImage: \"2.circle.fill\") { print(\"Button 2 pressed\") }\n        } label: { fade in\n            Text(\"Present Menu!\")\n                .opacity(fade ? 0.5 : 1)\n        }\n    }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSwiftUI (Customized)\u003c/summary\u003e\n\n```swift\nTemplates.Menu(\n    configuration: {\n        $0.width = 360\n        $0.backgroundColor = .blue.opacity(0.2)\n    }\n) {\n    Text(\"Hi, I'm a menu!\")\n        .padding()\n\n    Templates.MenuDivider()\n\n    Templates.MenuItem {\n        print(\"Item tapped\")\n    } label: { fade in\n        Color.clear.overlay(\n            AsyncImage(url: URL(string: \"https://getfind.app/image.png\")) {\n                $0.resizable().aspectRatio(contentMode: .fill)\n            } placeholder: {\n                Color.clear\n            }\n        )\n        .frame(height: 180)\n        .clipped()\n        .opacity(fade ? 0.5 : 1)\n    }\n\n} label: { fade in\n    Text(\"Present Menu!\")\n        .opacity(fade ? 0.5 : 1)\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSwiftUI (Manual Presentation)\u003c/summary\u003e\n\n```swift\nstruct ContentView: View {\n    @State var present = false\n    var body: some View {\n        VStack {\n            Toggle(\"Activate\", isOn: $present)\n                .padding()\n                .background(.regularMaterial)\n                .cornerRadius(12)\n                .padding()\n            \n            Templates.Menu(present: $present) {\n                Templates.MenuButton(title: \"Button 1\", systemImage: \"1.circle.fill\") { print(\"Button 1 pressed\") }\n                Templates.MenuButton(title: \"Button 2\", systemImage: \"2.circle.fill\") { print(\"Button 2 pressed\") }\n            } label: { fade in\n                Text(\"Present Menu!\")\n                    .opacity(fade ? 0.5 : 1)\n            }\n        }\n    }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eUIKit (Basic)\u003c/summary\u003e\n\n```swift\nclass ViewController: UIViewController {\n    @IBOutlet var label: UILabel!\n\n    lazy var menu = Templates.UIKitMenu(sourceView: label) {\n        Templates.MenuButton(title: \"Button 1\", systemImage: \"1.circle.fill\") { print(\"Button 1 pressed\") }\n        Templates.MenuButton(title: \"Button 2\", systemImage: \"2.circle.fill\") { print(\"Button 2 pressed\") }\n    } fadeLabel: { [weak self] fade in\n        self?.label.alpha = fade ? 0.5 : 1\n    }\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        _ = menu /// Create the menu.\n    }\n}\n```\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003eUIKit (Customized)\u003c/summary\u003e\n\n```swift\nclass ViewController: UIViewController {\n    @IBOutlet var label: UILabel!\n\n    lazy var menu = Templates.UIKitMenu(\n        sourceView: label,\n        configuration: {\n            $0.width = 360\n            $0.backgroundColor = .blue.opacity(0.2)\n        }\n    ) {\n        Text(\"Hi, I'm a menu!\")\n            .padding()\n\n        Templates.MenuDivider()\n\n        Templates.MenuItem {\n            print(\"Item tapped\")\n        } label: { fade in\n            Color.clear.overlay(\n                AsyncImage(url: URL(string: \"https://getfind.app/image.png\")) {\n                    $0.resizable().aspectRatio(contentMode: .fill)\n                } placeholder: {\n                    Color.clear\n                }\n            )\n            .frame(height: 180)\n            .clipped()\n            .opacity(fade ? 0.5 : 1)\n        }\n    } fadeLabel: { [weak self] fade in\n        UIView.animate(withDuration: 0.15) {\n            self?.label.alpha = fade ? 0.5 : 1\n        }\n    }\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        _ = menu /// Create the menu.\n    }\n}\n\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eUIKit (Manual Presentation)\u003c/summary\u003e\n\n```swift\nclass ViewController: UIViewController {\n    /// ...\n\n    @IBAction func switchPressed(_ sender: UISwitch) {\n        if menu.isPresented {\n            menu.dismiss()\n        } else {\n            menu.present()\n        }\n    }\n}\n```\n\n\u003c/details\u003e\n\n\nBasic | Customized | Manual Presentation\n--- | --- | ---\n![Menu with 2 buttons](Assets/MenuBasic.png) | ![Menu with image and divider](Assets/MenuCustomized.png) | ![Manually activate the menu with a toggle switch](Assets/MenuManual.png)\n\n\n### 🧩 Animating Between Popovers\nAs long as the view structure is the same, you can smoothly transition from one popover to another. \n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nSwiftUI\n\u003c/strong\u003e\n\u003cbr\u003e\nUse the \u003ccode\u003e.popover(selection:tag:attributes:view:)\u003c/code\u003e modifier. \n\u003c/td\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nUIKit\n\u003c/strong\u003e\n\u003cbr\u003e\nGet the existing popover using \u003ccode\u003eUIResponder.popover(tagged:)\u003c/code\u003e, then call \u003ccode\u003eUIResponder.replace(_:with:)\u003c/code\u003e.\n\u003c/td\u003e\n\u003c/tr\u003e\n  \n\u003ctr\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```swift\nstruct ContentView: View {\n    @State var selection: String?\n    \n    var body: some View {\n        HStack {\n            Button(\"Present First Popover\") { selection = \"1\" }\n            .popover(selection: $selection, tag: \"1\") {\n\n                /// Will be presented when selection == \"1\".\n                Text(\"Hi, I'm a popover.\")\n                    .background(.blue)\n            }\n            \n            Button(\"Present Second Popover\") { selection = \"2\" }\n            .popover(selection: $selection, tag: \"2\") {\n\n                /// Will be presented when selection == \"2\".\n                Text(\"Hi, I'm a popover.\")\n                    .background(.green)\n            }\n        }\n    }\n}\n```\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```swift\n@IBAction func button1Pressed(_ sender: Any) {\n    var newPopover = Popover { Text(\"Hi, I'm a popover.\").background(.blue) }\n    newPopover.attributes.sourceFrame = { [weak button1] in button1.windowFrame() }\n    newPopover.attributes.dismissal.excludedFrames = { [weak button2] in [button2.windowFrame()] }\n    newPopover.attributes.tag = \"Popover 1\"\n    \n    if let oldPopover = popover(tagged: \"Popover 2\") {\n        replace(oldPopover, with: newPopover)\n    } else {\n        present(newPopover) /// Present if the old popover doesn't exist.\n    }\n}\n@IBAction func button2Pressed(_ sender: Any) {\n    var newPopover = Popover { Text(\"Hi, I'm a popover.\").background(.green) }\n    newPopover.attributes.sourceFrame = { [weak button2] in button2.windowFrame() }\n    newPopover.attributes.dismissal.excludedFrames = { [weak button1] in [button1.windowFrame()] }\n    newPopover.attributes.tag = \"Popover 2\"\n    \n    if let oldPopover = popover(tagged: \"Popover 1\") {\n        replace(oldPopover, with: newPopover)\n    } else {\n        present(newPopover)\n    }\n}\n```\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n| \u003cimg src=\"Assets/AnimatingBetweenPopovers.gif\" width=300 alt=\"Smooth transition between popovers (from blue to green and back.\"\u003e |\n| --- |\n\n### 🌃 Background\nYou can put anything in a popover's background.\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nSwiftUI\n\u003c/strong\u003e\n\u003cbr\u003e\nUse the \u003ccode\u003e.popover(present:attributes:view:background:)\u003c/code\u003e modifier. \n\u003c/td\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nUIKit\n\u003c/strong\u003e\n\u003cbr\u003e\nUse the \u003ccode\u003ePopover(attributes:view:background:)\u003c/code\u003e initializer. \n\u003c/td\u003e\n\u003c/tr\u003e\n  \n\u003ctr\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```swift\n.popover(present: $present) {\n    PopoverView()\n} background: { /// here!\n    Color.green.opacity(0.5)\n}\n```\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```swift\nvar popover = Popover {\n    PopoverView()\n} background: { /// here!\n    Color.green.opacity(0.5)\n}\n```\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n\u003cimg src=\"Assets/PopoverBackground.png\" width=200 alt=\"Green background over the entire screen, but underneath the popover\"\u003e\n\n\n### 📖 Popover Reader\nThis reads the popover's context, which contains its frame, window, attributes, and various other properties. It's kind of like [`GeometryReader`](https://www.hackingwithswift.com/quick-start/swiftui/how-to-provide-relative-sizes-using-geometryreader), but cooler. You can put it in the popover's view or its background.\n\n```swift\n.popover(present: $present) {\n    PopoverView()\n} background: {\n    PopoverReader { context in\n        Path {\n            $0.move(to: context.frame.point(at: .bottom))\n            $0.addLine(to: context.windowBounds.point(at: .bottom))\n        }\n        .stroke(Color.blue, lineWidth: 4)\n    }\n}\n```\n\n| \u003cimg src=\"Assets/PopoverReader.gif\" width=200 alt=\"Line connects the bottom of the popover with the bottom of the screen\"\u003e |\n| --- |\n\n### 🏷 Frame Tags\nPopovers 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.\n\n```swift\nText(\"This is a view\")\n    .frameTag(\"Your Tag Name\") /// Adds a tag inside the window.\n\n/// ...\n\nWindowReader { window in\n    Text(\"Click me!\")\n    .popover(\n        present: $present,\n        attributes: {\n            $0.sourceFrame = window.frameTagged(\"Your Tag Name\") /// Retrieves a tag from the window.\n        }\n    )\n}\n```\n\n\n### 📄 Templates\nGet started quickly with some templates. All of them are inside [`Templates`](Sources/Templates) with example usage in the example app.\n\n- `AlertButtonStyle` - a button style resembling a system alert.\n- `VisualEffectView` - lets you use UIKit blurs in SwiftUI.\n- `Container` - a wrapper view for the `BackgroundWithArrow` shape.\n- `Shadow` - an easier way to apply shadows.\n- `BackgroundWithArrow` - a shape with an arrow that looks like the system popover.\n- `CurveConnector` - an animatable shape with endpoints that you can set.\n- `Menu` - the system menu, but built from scratch.\n\n\u003cbr\u003e\n\n## Notes\n### State Re-Rendering\nIf you directly pass a variable down to the popover's view, it might not update. Instead, move the view into its own struct and pass down a `Binding`.\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nYes\n\u003c/strong\u003e\n\u003cbr\u003e\nThe popover's view is in a separate struct, with \u003ccode\u003e$string\u003c/code\u003e passed down.\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cstrong\u003e\nNo\n\u003c/strong\u003e\n\u003cbr\u003e\nThe button is directly inside the \u003ccode\u003eview\u003c/code\u003e parameter and receives \u003ccode\u003estring\u003c/code\u003e.\n\u003c/td\u003e\n\u003c/tr\u003e\n  \n\u003ctr\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```swift\nstruct ContentView: View {\n    @State var present = false\n    @State var string = \"Hello, I'm a popover.\"\n\n    var body: some View {\n        Button(\"Present popover!\") { present = true }\n        .popover(present: $present) {\n            PopoverView(string: $string) /// Pass down a Binding ($).\n        }\n    }\n}\n\n/// Create a separate view to ensure that the button updates.\nstruct PopoverView: View {\n    @Binding var string: String\n\n    var body: some View {\n        Button(string) { string = \"The string changed.\" }\n        .background(.mint)\n        .cornerRadius(16)\n    }\n}\n```\n\u003c/td\u003e\n\u003ctd\u003e\n\u003cbr\u003e\n\n```swift\nstruct ContentView: View {\n    @State var present = false\n    @State var string = \"Hello, I'm a popover.\"\n\n    var body: some View {\n        Button(\"Present popover!\") {\n            present = true\n        }\n        .popover(present: $present) {\n\n            /// Directly passing down the variable (without $) is unsupported.\n            /// The button might not update.\n            Button(string) { \n                string = \"The string changed.\"\n            }\n            .background(.mint)\n            .cornerRadius(16)\n        }\n    }\n}\n```\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n### Supporting Multiple Screens • [*`v1.1.0`*](https://github.com/aheze/Popovers/releases/tag/1.1.0)\nPopovers comes with built-in support for multiple screens, but retrieving frame tags requires a reference to the hosting window. You can get this via `WindowReader` or `PopoverReader`'s context.\n\n```swift\nWindowReader { window in \n\n}\n\n/// If inside a popover's `view` or `background`, use `PopoverReader` instead.\nPopoverReader { context in\n    let window = context.window\n}\n```\n\n### Popover Hierarchy\nManage a popover's z-axis level by attaching [`.zIndex(_:)`](https://developer.apple.com/documentation/swiftui/view/zindex(_:)) to its view. A higher index will bring it forwards.\n\n\n## Community\n\nAuthor | Contributing | Need Help?\n--- | --- | ---\nPopovers is made by [aheze](https://github.com/aheze). | All contributions are welcome. Just [fork](https://github.com/aheze/Popovers/fork) the repo, then make a pull request. | Open an [issue](https://github.com/aheze/Popovers/issues) or join the [Discord server](https://discord.com/invite/Pmq8fYcus2). You can also ping me on [Twitter](https://twitter.com/aheze0). Or read the source code — there's lots of comments.\n\n### Apps Using Popovers\n\n[Find](http://getfind.app) 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!\n\n\u003ca href=\"http://getfind.app\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/aheze/OpenFind/main/Assets/v3%20Thumbnail.jpg\" height=\"300\" alt=\"Find App\"\u003e\n\u003c/a\u003e\n\n[AnyTracker](https://apps.apple.com/app/anytracker-track-anything/id6450756953) is an app that tracks numbers, text and prices on websites. It uses Popovers to display sleek dialogs with a nice background blur.\n\n\u003ca href=\"https://anytracker.org/\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/shervinkoushan/AnyTracker-assets/main/Feature%20graphic.png\" height=\"260\" alt=\"AnyTracker\"\u003e\n\u003c/a\u003e\n\n\u003cp\u003e\u0026nbsp;\u003c/p\u003e\n\n[Track Attack!](https://apps.apple.com/us/app/track-attack/id1557802135) is the ultimate vehicle enthusiast app for the road and track. Modern UI. Dual front \u0026 rear cameras. GPS \u0026 sensor data. RPM and throttle. Heart rate with Apple Watch. Lap timing for the track \u0026 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.\n\n\u003ca href=\"https://www.trackattack.app\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/velocitylabs-ai/trackattack/bbfd440c42b4f0a1bbd70566aea3846b9a983c1c/assets/track-attack-porsche-logo.jpg\" height=\"260\" alt=\"TrackAttack\"\u003e\n\u003c/a\u003e\n\n\u003cp\u003e\u0026nbsp;\u003c/p\u003e\n\nIf you have an app that uses Popovers, just make a PR or [message me](https://twitter.com/aheze0).\n\n## License\n\n```\nMIT License\n\nCopyright (c) 2023 A. Zheng\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n\n![Stats](https://repobeats.axiom.co/api/embed/7b2a5202040079bf91e27a5195d30e8ebd06dd06.svg \"Stats\")\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faheze%2FPopovers","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faheze%2FPopovers","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faheze%2FPopovers/lists"}