{"id":16697830,"url":"https://github.com/leonatan/lnpopupui","last_synced_at":"2025-05-16T08:05:59.005Z","repository":{"id":46092241,"uuid":"285600898","full_name":"LeoNatan/LNPopupUI","owner":"LeoNatan","description":"A SwiftUI library for presenting views as popups, much like the Apple Music and Podcasts apps.","archived":false,"fork":false,"pushed_at":"2025-04-09T14:10:20.000Z","size":238270,"stargazers_count":471,"open_issues_count":0,"forks_count":31,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-05-16T08:05:52.845Z","etag":null,"topics":["lnpopupcontroller","lnpopupcontroller-framework","popup","spm","swiftui","swiftui-library"],"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/LeoNatan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"LeoNatan","custom":["paypal.me/LeoNatan25"]}},"created_at":"2020-08-06T15:04:55.000Z","updated_at":"2025-05-15T12:38:17.000Z","dependencies_parsed_at":"2024-09-18T02:02:17.243Z","dependency_job_id":"14b31c36-2740-4a64-b2df-8b86f469aec2","html_url":"https://github.com/LeoNatan/LNPopupUI","commit_stats":{"total_commits":96,"total_committers":3,"mean_commits":32.0,"dds":"0.13541666666666663","last_synced_commit":"36dae8ed8bf207c0579edf4d96919bd59876ea7c"},"previous_names":[],"tags_count":70,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LeoNatan%2FLNPopupUI","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LeoNatan%2FLNPopupUI/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LeoNatan%2FLNPopupUI/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LeoNatan%2FLNPopupUI/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LeoNatan","download_url":"https://codeload.github.com/LeoNatan/LNPopupUI/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254493378,"owners_count":22080126,"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":["lnpopupcontroller","lnpopupcontroller-framework","popup","spm","swiftui","swiftui-library"],"created_at":"2024-10-12T17:49:24.391Z","updated_at":"2025-05-16T08:05:53.996Z","avatar_url":"https://github.com/LeoNatan.png","language":"Swift","funding_links":["https://github.com/sponsors/LeoNatan","paypal.me/LeoNatan25","https://paypal.me/LeoNatan25"],"categories":[],"sub_categories":[],"readme":"# LNPopupUI\n\n`LNPopupUI` is a SwiftUI library for presenting views as popups, much like the Apple Music and Podcasts apps.\n\nThis is a SwiftUI wrapper of the [LNPopupController framework](https://github.com/LeoNatan/LNPopupController), adapted to work with SwiftUI.\n\n[![GitHub release](https://img.shields.io/github/release/LeoNatan/LNPopupUI.svg)](https://github.com/LeoNatan/LNPopupUI/releases) [![GitHub stars](https://img.shields.io/github/stars/LeoNatan/LNPopupUI.svg)](https://github.com/LeoNatan/LNPopupUI/stargazers) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/LeoNatan/LNPopupUI/master/LICENSE) \u003cspan class=\"badge-paypal\"\u003e\u003ca href=\"https://paypal.me/LeoNatan25\" title=\"Donate to this project using PayPal\"\u003e\u003cimg src=\"https://img.shields.io/badge/paypal-donate-yellow.svg?style=flat\" alt=\"PayPal Donation Button\" /\u003e\u003c/a\u003e\u003c/span\u003e\n\n[![GitHub issues](https://img.shields.io/github/issues-raw/LeoNatan/LNPopupUI.svg)](https://github.com/LeoNatan/LNPopupUI/issues) [![GitHub contributors](https://img.shields.io/github/contributors/LeoNatan/LNPopupUI.svg)](https://github.com/LeoNatan/LNPopupUI/graphs/contributors) [![Swift Package Manager compatible](https://img.shields.io/badge/swift%20package%20manager-compatible-green)](https://swift.org/package-manager/)\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/open_floating_popup.gif\" width=\"360\"/\u003e\u003c/p\u003e\n\nOnce a popup bar is presented with a content view, the user can swipe or tap the popup bar at any point to present the content view. After finishing, the user dismisses the popup by either swiping the content view or tapping the popup close button.\n\nThe library extends SwiftUI’s `View` with new functionality for presenting and customizing popups with content views, as well as setting information such as the popup bar’s title, image and bar button items. When a popup bar is presented, the popup bar automatically adapts to the view it was presented on for best appearance.\n\nGenerally, it is recommended to present the popup bar on the outermost view, such as `TabView` or `NavigationStack`. For example, if you have a view contained in a navigation stack, which is in turn contained in a tab view, it is recommended to present the popup bar on the tab view.\n\nCheck the demo project for a quick recreation of Apple’s music app.\n\n### Features\n\n* Available for iOS 14 and above, as a SPM package for SwiftUI\n* A SwiftUI library, wrapping the [LNPopupController framework](https://github.com/LeoNatan/LNPopupController); the library works internally with SwiftUI’s generated UIKit content to present the framework in a native manner\n\n## Adding to Your Project\n\n### Swift Package Manager\n\n`LNPopupUI` supports SPM versions 5.5 (Xcode 13) and above. In Xcode, click `File` -\u003e `Swift Packages` -\u003e `Add Package Dependency`, enter `https://github.com/LeoNatan/LNPopupUI`. Select the version you’d like to use.\n\nYou can also manually add the package to your Package.swift file:\n\n```swift\n.package(url: \"https://github.com/LeoNatan/LNPopupUI.git\", from: \"1.5.0\")\n```\n\nAnd the dependency in your target:\n\n```swift\n.target(name: \"BestExampleApp\", dependencies: [\"LNPopupUI\"]),\n```\n\n## Using the Library\n\n### Project Integration\n\nImport the module in your project:\n\n```swift\nimport LNPopupUI\n```\n\n### Popups\n\nPopups consist of a popup bar and a popup content view. Information for the popup bar, such as the title, image and bar button items, is configured using the provided modifier APIs.\n\nTo present the popup bar, use the `popup(isBarPresented:isPopupOpen:content:)` modifier. The user is then able to interact with the popup bar and popup content. \n\nTo present and dismiss the popup bar programmatically, toggle the `isPopupBarPresented` bound var. To open or close the popup programmatically, toggle the `isPopupOpen` bound var.\n\nFor more information, see the documentation in [LNPopupUI.swift](https://github.com/LeoNatan/LNPopupUI/blob/master/Sources/LNPopupUI/LNPopupUI.swift).\n\n\n```swift\nTabView {\n    //Container content  \n    AlbumViews()\n}\n.popup(isBarPresented: $isPopupBarPresented, isPopupOpen: $isPopupOpen) {\n    //Popup content, visible when the popup opens\n    PlayerView(song: currentSong)\n        .popupTitle(currentSong.title)\n        .popupSubtitle(currentSong.subtitle)\n        .popupImage(Image(currentSong.imageName))\n        .popupBarItems {\n            ToolbarItemGroup(placement: .popupBar) {\n                Button {\n                    isPlaying.toggle()\n                } label: {\n                    Image(systemName: \"play.fill\")\n                }\n            \n                Button {\n                    nextSong()\n                } label: {\n                    Image(systemName: \"forward.fill\")\n                }\n            }\n        }\n}\n```\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/floating_no_scroll.gif\" width=\"360\"/\u003e\u003c/p\u003e\n\n### Appearance and Behavior\n\n`LNPopupUI` provides three distinct styles of popup look and feel, each based on Music app looks and feels, that Apple has introduced over the years. Popup bar styles are labeled \"floating”, “prominent\" and \"compact\", matching the appropriate Apple style. Popup interaction styles are labeled \"snap\" for modern style snapping popups and \"drag\" for iOS 9 interactive popup interaction. Popup close buttons styles are labeled \"chevron\" for modern style chevron close button and \"round\" for iOS 9-style close buttons. For each, there is a \"default\" style for choosing the most suitable one for the current platform and operating system version.\n\nThe defaults are:\n\n- iOS 17:\n\n  * Floating bar style\n\n  * Snap interaction style\n\n  * Chevron close button style\n\n  * No progress view style\n\n- iOS 16 and below:\n\n  - Prominent bar style\n\n  * Snap interaction style\n\n  * Chevron close button style\n\n  * No progress view style\n\nYou can also present completely custom popup bars. For more information, see [Custom Popup Bar View](#custom-popup-bar-view).\n\nBy default, for navigation and tab views, the appearance of the popup bar is determined according to the container’s bottom bar's appearance. For other container views, a default appearance is used, most suitable for the current environment.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/floating_bar_style.gif\" width=\"360\"/\u003e\u003c/p\u003e\n\nTo disable inheriting the bottom bar’s appearance, call the `popupBarInheritsAppearanceFromDockingView()` modifier with `false`.\n\n#### Bar Style\n\nCustomizing the popup bar style is achieved by calling the `.popupBarStyle()` modifier.\n\n```swift\n.popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {\n    //Popup content view\n}\n.popupBarStyle(.floating)\n```\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/floating_no_scroll.gif\" width=\"360\"/\u003e \u003cimg src=\"./Supplements/modern_no_scroll.gif\" width=\"360\"/\u003e \u003cimg src=\"./Supplements/scroll.gif\" width=\"360\"/\u003e\u003c/p\u003e\n\n#### Interaction Style\n\nCustomizing the popup interaction style is achieved by calling the `.popupInteractionStyle()` modifier.\n\n```swift\n.popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {\n    //Popup content view\n}\n.popupInteractionStyle(.drag)\n```\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/interaction_snap.gif\" width=\"360\"/\u003e \u003cimg src=\"./Supplements/interaction_drag.gif\" width=\"360\"/\u003e\u003c/p\u003e\n\n#### Progress View Style\n\nCustomizing the popup bar progress view style is achieved by calling the `.popupBarProgressViewStyle()` modifier.\n\n```swift\n.popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {\n    //Popup content view\n}\n.popupBarProgressViewStyle(.top)\n```\n\nTo hide the progress view, set the bar progress view style to `.none`.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/progress_view_none.png\" width=\"360\"/\u003e\u003cbr/\u003e\u003cbr/\u003e\u003cimg src=\"./Supplements/progress_view_top.png\" width=\"360\"/\u003e\u003cbr/\u003e\u003cbr/\u003e\u003cimg src=\"./Supplements/progress_view_bottom.png\" width=\"360\"/\u003e\u003c/p\u003e\n\n#### Close Button Style\n\nCustomizing the popup close button style is achieved by calling the `.popupCloseButtonStyle()` modifier.\n\n```swift\n.popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {\n    //Popup content view\n}\n.popupCloseButtonStyle(.round)\n```\n\nTo hide the popup close button, set the `popupCloseButtonStyle` to `.none`.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/close_button_none.png\" width=\"360\"/\u003e\u003cbr/\u003e\u003cbr/\u003e\u003cimg src=\"./Supplements/close_button_chevron.png\" width=\"360\"/\u003e\u003cbr/\u003e\u003cbr/\u003e\u003cimg src=\"./Supplements/close_button_round.png\" width=\"360\"/\u003e\u003c/p\u003e\n\n#### Text Marquee Scroll\n\nSupplying long text for the title and/or subtitle will result in a scrolling text, if text marquee scroll is enabled. Otherwise, the text will be truncated. To enable text marquee scrolling, use the `popupBarMarqueeScrollEnabled()` modifier.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/floating_no_scroll.gif\" width=\"360\"/\u003e \u003cimg src=\"./Supplements/scroll.gif\" width=\"360\"/\u003e\u003c/p\u003e\n\n#### Popup Transitions\n\nThe library supports popup image transitions:\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/popup_transitions.gif\"/\u003e\u003c/p\u003e\n\nTransitions are opt-in and require you apply the `.popupTransitionTarget()` modifier to your `Image` view in your popup content view, which is discovered automatically by the system and used as the target/source view for popup transitions.\n\n\u003e [!TIP]\n\u003e There must be a single `.popupTransitionTarget()` call inside your popup content view, or results will be undefined.\n\nThe system supports `.clipShape()` with basic shapes and a single `shadow()` modifier applied to the `Image` view.\n\n```swift\n.popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {\n\tImage(\"genre\\(demoContent.imageNumber)\")\n\t\t.resizable()\n\t\t.popupTransitionTarget()\n\t\t.aspectRatio(contentMode: .fit)\n\t\t.clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous))\n\t\t.shadow(color: enableCustomizations ? .indigo : .black.opacity(0.5), radius: 20)\n}\n```\n\n\u003e [!CAUTION]\n\u003e Using a complex clip shapes and/or multiple calls to `.shadow()` will result in undefined behavior and visual artifacts in your transitions.\n\nTransitions are only available for prominent and floating popup bar styles with drag interaction style. Any other combination will result in no transition and this method will not be called by the system.\n\n#### Popup Bar Customization\n\n`LNPopupUI` exposes many APIs to customize the default popup bar's appearance. \n\n```swift\n.popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {\n    //Popup content view\n}\n.popupBarInheritsAppearanceFromDockingView(false)\n.popupBarTitleTextAttributes(AttributeContainer()\n    .font(Font.custom(\"Chalkduster\", size: 14, relativeTo: .headline))\n    .foregroundColor(.yellow)\n    .paragraphStyle(customizationParagraphStyle))\n.popupBarSubtitleTextAttributes(AttributeContainer()\n    .font(.custom(\"Chalkduster\", size: 12, relativeTo: .subheadline))\n    .foregroundColor(.green)\n    .paragraphStyle(customizationParagraphStyle))\n.popupBarFloatingBackgroundShadow(color: .red, radius: 8)\n.popupBarImageShadow(color: .yellow, radius: 5)\n.popupBarFloatingBackgroundEffect(UIBlurEffect(style: .dark))\n.popupBarBackgroundEffect(UIBlurEffect(style: .dark))\n```\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/floating_custom.png\" width=\"360\"/\u003e \u003cimg src=\"./Supplements/modern_custom.png\" width=\"360\"/\u003e \u003cimg src=\"./Supplements/compact_custom.png\" width=\"360\"/\u003e\u003c/p\u003e\n\n#### Context Menus\n\nYou can add a context menu to your popup bar by calling the `.popupBarContextMenu()` modifier.\n\n```swift\n.popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {\n    //Popup content view\n}\n.popupBarContextMenu {\n    Button {\n        print(\"Context Menu Item 1\")\n    } label: {\n        Text(\"Context Menu Item 1\")\n        Image(systemName: \"globe\")\n    }\n    \n    Button {\n        print(\"Context Menu Item 2\")\n    } label: {\n        Text(\"Context Menu Item 2\")\n        Image(systemName: \"location.circle\")\n    }\n}\n```\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/popup_bar_context_menu.png\" width=\"360\"/\u003e\u003c/p\u003e\n\n#### ProMotion Support\n\n`LNPopupUI` fully supports ProMotion on iPhone and iPad.\n\nFor iPhone 13 Pro and above, you need to add the `CADisableMinimumFrameDurationOnPhone` key to your Info.plist and set it to `true`. See [Optimizing ProMotion Refresh Rates for iPhone 13 Pro and iPad Pro](https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro?language=objc) for more information. `LNPopupUI` will log a single warning message in the console if this key is missing, or is set to `false`.\n\n#### Full Right-to-Left Support\n\nThe library has full right-to-left support.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/rtl_english.png\" width=\"360\"/\u003e \u003cimg src=\"./Supplements/rtl_hebrew.png\" width=\"360\"/\u003e\u003c/p\u003e\n\n#### Custom Popup Bar View\n\nYou can display your own view as the popup bar, instead of the system-provided ones, by using the `.popupBarCustomView()` modifier.\n\n```swift\n.popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {\n    //Popup content view\n}\n.popupBarCustomView(wantsDefaultTapGesture: false, wantsDefaultPanGesture: false, wantsDefaultHighlightGesture: false) {\n  //Custom popup bar view content\n}\n```\n\nThe `wantsDefaultTapGesture`, `wantsDefaultPanGesture` and `wantsDefaultHighlightGesture` arguments control whether the default system gestures of the popup bar should be enabled or disabled.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Supplements/custom_bar.png\" width=\"360\"/\u003e\u003c/p\u003e\n\n\u003e [!TIP]\n\u003e Only implement a custom popup bar if you need a design that is significantly different than the provided [standard popup bar styles](#bar-style). A lot of care and effort has been put into integrating these popup bar styles with the SwiftUI view system, including look, feel, transitions and interactions. Custom bars provide a blank canvas for you to implement a bar view of your own, but if you end up recreating a bar design that is similar to a standard bar style, you are more than likely losing subtleties that have been added and perfected over the years in the standard implementations. Instead, consider using the [many customization APIs](#popup-bar-customization) to tweak the standard bar styles to fit your app’s design.\n\nThe included demo project includes an example custom popup bar scene.\n\n#### Low-Level Bar Customization\n\n`LNPopupUI` exposes the `.popupBarCustomizer()` modifier, which allows lower-level customization through the UIKit `LNPopupBar` object.\n\n```swift\n.popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {\n    //Popup content view\n}\n.popupBarCustomizer { popupBar in\n    popupBar.popupOpenGestureRecognizer.delegate = self.gestureRecognizerDelegateHelper\n    popupBar.barHighlightGestureRecognizer.isEnabled = false\n}\n```\n\n\u003e [!TIP]\n\u003e The `.popupBarCustomizer()` modifier exposes the underlying `LNPopupBar` from the `LNPopupController` framework. This framework allows modifying properties that are not exposed natively in SwiftUI, such as direct gesture recognizer control. While it is possible to customize the appearance the bar using this modifier, this API only accepts UIKit data types, such as `UIColor` and `UIFont`. Instead, use the SwiftUI-native customization APIs, which support SwiftUI-native data types, such as `Color` and `Font`, and are better integrated with rest of the SwiftUI view model.\n\n### `LNPopupController` additions\n\nIn addition to the main SwiftUI functionality, the library offers extensions to `LNPopupController` for hosting SwiftUI views as popup content and custom popup bar content.\n\nUse `LNPopupContentHostingController` to create a popup content hosting controller:\n\n```swift\nlet controller = LNPopupContentHostingController {\n    PlayerView(song: currentSong)\n        .popupTitle(currentSong.name, subtitle: currentSong.album.name)\n        .popupImage(currentSong.artwork ?? currentSong.album.artwork)\n}\n\ntabBarController?.presentPopupBar(with: controller, animated: true)\n```\n\nOr use `UIViewController.presentPopupBar(with:animated:)` directly:\n\n```swift\ntabBarController?.presentPopupBar(with: {\n    PlayerView(song: currentSong)\n        .popupTitle(currentSong.name, subtitle: currentSong.album.name)\n        .popupImage(currentSong.artwork ?? currentSong.album.artwork)\n}, animated: true)\n```\n\nUse `LNPopupCustomBarHostingController` to create a custom popup bar hosting controller:\n\n```swift\ntabBarController?.popupBar.customBarViewController = LNPopupCustomBarHostingController {\n    MyCustomPlaybackControlsView()\n}\n```\n\n## Acknowledgements\n\nThe library uses:\n* [MarqueeLabel](https://github.com/cbpowell/MarqueeLabel) Copyright (c) 2011-2020 Charles Powell\n\nAdditionally, the demo project uses:\n\n* [LoremIpsum](https://github.com/lukaskubanek/LoremIpsum) Copyright (c) 2013-2020 Lukas Kubanek\n* [swiftui-introspect](https://github.com/siteline/swiftui-introspect) Copyright 2019 Timber Software\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=LeoNatan/LNPopupUI\u0026type=Date)](https://star-history.com/#LeoNatan/LNPopupUI\u0026Date)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleonatan%2Flnpopupui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleonatan%2Flnpopupui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleonatan%2Flnpopupui/lists"}