LNPopupUI

repository·master·Indexed 20 days ago

https://github.com/leonatan/lnpopupui

A SwiftUI library for presenting views as popups, mimicking UI patterns found in Apple Music and Podcasts. It features a customizable popup bar and content presentation system with support for paging, image transitions, and floating styles. The library provides modifiers for configuring popup items, interaction styles, and progress views, as well as support for custom popup bar views and low-level UIKit customization via popupBarCustomizer.

Tokens
4.3K
Snippets
18
Records
19
Agent score
20%

What's inside LNPopupUI

  1. How popup presentations work in LNPopupUI

    master

    A popup presentation is composed of several key abstractions:

    • Popup container view: The View that hosts the presentation (e.g., a TabView or NavigationStack). It is recommended to apply the popup modifier to the outermost view.
    • Popup content controller: The View that represents the content shown when the popup is fully open.
    • Popup bar: A bar docked to the bottom of the container view. It displays at-a-glance information and allows user interaction (swiping/tapping) to open the content view.
    • Popup items: The data source that provides the information (title, image, etc.) displayed on the popup bar.
    • Custom popup bar view: An optional custom implementation of the bar.

    To implement a popup, use the .popup(isBarPresented:isPopupOpen:content:) modifier. You control the visibility of the bar and the open state by toggling the bound boolean variables.

    // Example of the core structure
    TabView {
      // Container content  
    }
    .popup(isBarPresented: $isPopupBarPresented, isPopupOpen: $isPopupOpen) {
      // Popup content view
      PlayerView()
        .popupItems(selection: $currentSong) { 
           // ... items ...
        }
    }
  2. Configure Popup Items

    master

    Popup items provide the data displayed on the popup bar. You must choose one of the following three modifier families and never mix them within the same popup content hierarchy.

    1. Single Popup Item

    Use .popupItem(popupItem:) to provide a single PopupItem instance. This is useful for static information and does not support paging.

    2. Multiple Popup Items (with Paging)

    Use .popupItems(selection:items:) to provide a collection of items. This enables paging support, allowing users to swipe left/right on the bar to switch items. The selection binding is updated when the user pages to a new item.

    3. Default Popup Item (Legacy)

    Use modifiers like .popupTitle(_:subtitle:), .popupImage(_:), and .popupBarButtons { ... } to update the default item. This approach is considered legacy; it is recommended to use the first two methods instead.

    Warning: If you do not provide any popup items, the popup bar will remain empty. If you are using a custom popup bar, popup item modifiers will have no effect.

  3. How Popup Bar Minimization works

    master

    Starting with iOS 26, the library supports popup bar minimization, currently supported with TabView container views.

    1. Enable Minimization: Set the behavior on the TabView using .tabBarMinimizeBehavior(_:) (e.g., .onScrollDown).
    2. React to Placement: Use the @Environment(\.popupBarPlacement) variable in your content or custom bar views to adjust UI elements based on whether the bar is .inline or .regular.
    3. Disable Inheritance: To prevent the popup bar from inheriting the bottom bar's appearance, use .popupBarInheritsAppearanceFromDockingView(false). To disable minimization entirely, use .popupBarInheritsBottomBarMetrics(false) on the TabView.
    // Enabling minimization on a TabView
    TabView {
      // ...
    }
    .tabBarMinimizeBehavior(.onScrollDown)
    
    // Adjusting content based on placement
    struct PlayerView: View {
      @Environment(\.popupBarPlacement) var popupBarPlacement
    
      var body: some View {
        ...
        .popupItem {
          PopupItem(identifier: "id", title: "Hello World") {
            ToolbarItemGroup(placement: .popupBar) {
              PlayButton()
              if popupBarPlacement != .inline {
                NextButton()
              }
            }
          }
        }
      }
    }
  4. Implement Popup Image Transitions

    master

    You can opt-in to image transitions by applying the .popupTransitionTarget() modifier to a single Image view within your popup content. The system uses this image as the source/target for the transition.

    Requirements & Limitations:

    • There must be exactly one .popupTransitionTarget() call in the content view.
    • Transitions are only available when using the drag interaction style.
    • Supported modifiers on the target image: .clipShape() (basic shapes) and a single .shadow().
    • Caution: Complex clip shapes or multiple shadows may cause undefined behavior.
    .popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {
      Image("genre_image")
        .resizable()
        .popupTransitionTarget()
        .aspectRatio(contentMode: .fit)
        .clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous))
        .shadow(color: .indigo, radius: 20)
    }
  5. Install LNPopupUI via Swift Package Manager

    master

    You can add LNPopupUI to your project using Xcode's built-in SPM support or by manually editing your Package.swift file.

    Via Xcode:

    1. Click FileAdd Package Dependencies….
    2. Enter https://github.com/LeoNatan/LNPopupUI.
    3. Select your desired version.

    Via Package.swift: Add the package to your dependencies and the target:

    // Add to dependencies
    .package(url: "https://github.com/LeoNatan/LNPopupUI.git", from: "2.5.0")
    
    // Add to your target
    .target(name: "MyExampleApp", dependencies: ["LNPopupUI"])

    After installation, import the module in your Swift files:

    import LNPopupUI
    .package(url: "https://github.com/LeoNatan/LNPopupUI.git", from: "2.5.0")
  6. Use the popupItem modifier for a single item

    master

    To display a single, non-paginated piece of information on the popup bar, use the .popupItem(popupItem:) modifier with a PopupItem instance.

    TabView {
      //Container content  
    }
    .popup(isBarPresented: $isPopupBarPresented, isPopupOpen: $isPopupOpen) {
      popupContent()
        .popupItem {
          PopupItem(id: "intro", image: Image("MyImage")) {
            Text("Welcome to ") + Text("LNPopupUI").fontWeight(.heavy) + Text("!")
          } buttons: {
            ToolbarItemGroup(placement: .popupBar) {
              Link(destination: url) {
                Label("LNPopupUI", systemImage: "suit.heart.fill")
              }
            }
          }
        }
    }
  7. Use the popupItems modifier for paging support

    master

    To create a paginated popup bar where users can swipe through a collection (like a music playlist), use the .popupItems(selection:items:) modifier inside your popup content view. The selection parameter must be a binding to the identifier of the currently selected item.

    TabView {
      //Container content  
    }
    .popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {
      ContentView()
        .popupItems(selection: $currentSong) { 
          for song in playlist {
            PopupItem(
              id: song, 
              title: song.name, 
              subtitle: song.albumName, 
              image: song.art, 
              progress: playbackState.progress
            ) { 
              playbackButtons(for: song, with: playbackState) 
            }
          }
        }
    }
  8. Add a Context Menu to the Popup Bar

    master

    Use the .popupBarContextMenu(menuItems:) modifier to attach a context menu to the popup bar.

    .popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {
    	//Popup content view
    }
    .popupBarContextMenu {
      Button {
        print("Action 1")
      } label: {
        Text("Action 1")
        Image(systemName: "globe")
      }
    }
  9. Customize the popup bar with SwiftUI

    master

    To replace the default popup bar UI with a custom SwiftUI view, use LNPopupCustomBarHostingController. Assign an instance of this controller to the customBarViewController property of the popupBar object on your LNPopupController.

    tabBarController?.popupBar.customBarViewController = LNPopupCustomBarHostingController {
      MyCustomPlaybackControlsView()
    }
  10. Customize Popup Bar Appearance

    master

    Use the following modifiers to tweak the standard popup bar's look and feel:

    • .popupBarInheritsAppearanceFromDockingView(_:): Control appearance inheritance.
    • .popupBarFloatingBackgroundShadow(color:radius:x:y:): Set shadow for floating bars.
    • .popupBarTitleTextAttributes(_:): Set title text styling using AttributeContainer.
    • .popupBarSubtitleTextAttributes(_:): Set subtitle text styling using AttributeContainer.
    • .popupBarImageShadow(color:radius:): Set shadow for images in the bar.
    • .popupBarFloatingBackgroundEffect(_:): Set blur/background effect for floating bars.
    • .popupBarBackgroundEffect(_:): Set background effect for standard bars.
    .popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {
        //Popup content view
    }
    .popupBarInheritsAppearanceFromDockingView(false)
    .popupBarFloatingBackgroundShadow(color: .red, radius: 8)
    .popupBarTitleTextAttributes(AttributeContainer().font(.headline).foregroundColor(.yellow))
    .popupBarFloatingBackgroundEffect(UIBlurEffect(style: .dark))
  11. Host SwiftUI views as popup content

    master

    You can host SwiftUI views within a popup by using LNPopupContentHostingController. This allows you to define your popup content using SwiftUI syntax and attach metadata via the .popupItem modifier.

    To present the popup, use presentPopupBar(with:) on your LNPopupController (or tabBarController).

    let controller = LNPopupContentHostingController {
      PlayerView(song: currentSong)
        .popupItem {
          PopupItem(id: "id", title: currentSong.name, subtitle: currentSong.albumName, image: currentSong.art)
        }
    }
    
    tabBarController?.presentPopupBar(with: controller)
  12. Low-Level Customization via popupBarCustomizer

    master

    For advanced control not exposed via SwiftUI, use .popupBarCustomizer(_:) to access the underlying UIKit LNPopupBar object. This is useful for modifying properties like gesture recognizer delegates.

    Warning: This API accepts UIKit data types (e.g., UIColor, UIFont) rather than SwiftUI types. For appearance customization, prefer the SwiftUI-native APIs.

    .popup(isBarPresented: $isPopupPresented, isPopupOpen: $isPopupOpen) {
      //Popup content view
    }
    .popupBarCustomizer { popupBar in
      popupBar.popupOpenGestureRecognizer.delegate = self.gestureRecognizerDelegateHelper
      popupBar.barHighlightGestureRecognizer.isEnabled = false
    }