FloatingPanel Documentation

repository·master·Indexed 26 days ago

https://github.com/scenee/floatingpanel

A UI component for iOS that provides floating, draggable, and dockable panels similar to Apple Maps, Shortcuts, and Stocks. It supports both SwiftUI and UIKit, offering features such as scroll view tracking, modal presentation, and customizable layouts via FloatingPanelController. The library includes a migration guide from version 1.x to 2.0, detailing API changes to FloatingPanelState, SurfaceView, and the redesigned anchor-based layout system.

Tokens
9.4K
Snippets
27
Records
34
Agent score
78%

What's inside FloatingPanel

  1. Present a floating panel modally in SwiftUI

    master

    To present a floating panel modally in SwiftUI, you must implement a custom coordinator by conforming to the FloatingPanelCoordinator protocol.

    Inside setupFloatingPanel, you configure the contentHostingController and use a Task to present the controller on the next run loop cycle to ensure the view hierarchy is correctly established.

    struct HomeView: View {
      var view: some View {
        MainView()
            .floatingPanel(
              coordinator: MyPanelCoordinator.self
            ) { proxy in
              ...
            }
      }
    }
    
    class MyPanelCoordinator: FloatingPanelCoordinator {
        ...
        func setupFloatingPanel<Main, Content>(
            mainHostingController: UIHostingController<Main>,
            contentHostingController: UIHostingController<Content>
        ) where Main: View, Content: View {
            // Set the delegate object
            controller.delegate = delegate
    
            // Set up the content
            contentHostingController.view.backgroundColor = .clear
            controller.set(contentViewController: contentHostingController)
    
            /* =============== HERE ==================== */
            // NOTE: 
            // Present the floating panel on the next run loop cycle
            // to ensure proper view hierarchy setup.
            Task { @MainActor in
                mainHostingController.present(controller, animated: false)
            }
        }
        ...
    }
  2. Manually show and hide a FloatingPanelController

    master

    If you need granular control, you can bypass addPanel and removePanelFromParent by manually managing the FloatingPanelController in the view hierarchy.

    To add the controller:

    1. Add fpc.view as a subview to your controller's view.
    2. Set fpc.view.frame to match your controller's view bounds.
    3. Use Auto Layout to constrain fpc.view to all four edges.
    4. Add the controller as a child using addChild(fpc).
    5. Call fpc.show(animated: true) and call fpc.didMove(toParent: self) in the completion block.

    To remove the controller:

    1. Call fpc.willMove(toParent: nil).
    2. Call fpc.hide(animated: true).
    3. In the completion block, remove fpc.view from its superview and call fpc.removeFromParent().
    // Add the floating panel view to the controller's view on top of other views.
    self.view.addSubview(fpc.view)
    
    // REQUIRED. It makes the floating panel view have the same size as the controller's view.
    fpc.view.frame = self.view.bounds
    
    // In addition, Auto Layout constraints are highly recommended.
    fpc.view.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      fpc.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0),
      fpc.view.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0.0),
      fpc.view.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0.0),
      fpc.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0),
    ])
    
    // Add the floating panel controller to the controller hierarchy.
    self.addChild(fpc)
    
    // Show the floating panel at the initial position defined in your `FloatingPanelLayout` object.
    fpc.show(animated: true) {
        // Inform the floating panel controller that the transition to the controller hierarchy has completed.
        fpc.didMove(toParent: self)
    }
  3. Migrate from FloatingPanel 1.x to 2.0

    master

    FloatingPanel 2.0 is a major release with breaking API changes.

    Updated Minimum Requirements

    • Swift 5.0
    • iOS 11 (iOS 10 is supported but not well tested)
    • Xcode 11.0

    Key API Changes

    • FloatingPanelPosition is now FloatingPanelState (used to specify panel position like top, left, bottom, right).
    • FloatingPanelSurfaceView is renamed to SurfaceView in Swift.
    • FloatingPanelBackdropView is renamed to BackdropView in Swift.
    • FloatingPanelGrabberHandleView is renamed to GrabberView in Swift.
    • The term "decelerate" has been replaced with "attract".
  4. Show a destination view controller in a secondary floating panel

    master

    By default, FloatingPanelController does not manage a view controller stack like UINavigationController. If you want to intercept 'Show' or 'Show Detail' segues from a content view controller to display the destination in a new floating panel instead of a standard navigation transition, you can override the show(_:sender:) method in your master view controller. This allows you to decouple the floating panel from the content view controller.

    class ViewController: UIViewController {
        var fpc: FloatingPanelController!
        var secondFpc: FloatingPanelController!
    
        // ...
    
        override func show(_ vc: UIViewController, sender: Any?) {
            secondFpc = FloatingPanelController()
            secondFpc.set(contentViewController: vc)
            secondFpc.addPanel(toParent: self)
        }
    }
  5. Install FloatingPanel via Swift Package Manager

    master

    To install FloatingPanel using Swift Package Manager, add the following dependency to your Package.swift manifest:

    1. Add the package to your dependencies:
    .package(url: "https://github.com/scenee/FloatingPanel", from: "3.2.1"),
    1. Add FloatingPanel as a dependency to your target:
    .target(name: "MyTarget", dependencies: [
      .product(name: "FloatingPanel", package: "FloatingPanel"),
      "AnotherModule"
    ]),
    1. Import the module in your source code using import FloatingPanel.
  6. Best practices for FloatingPanel SwiftUI implementation

    master

    When implementing a custom FloatingPanelCoordinator, follow these best practices:

    • Define Meaningful Events: Create an Event enum that captures only the interactions your SwiftUI views actually need to respond to.
    • Use Lazy Delegate Initialization: To allow your coordinator to act as the FloatingPanelControllerDelegate, initialize the delegate lazily: lazy var delegate: FloatingPanelControllerDelegate? = self.
    • Handle Environment Changes Efficiently: In onUpdate, compare the new environment values against the current panel state before performing updates to avoid redundant work.
    • Coordinate with SwiftUI Animations: On iOS 18+, use the transaction's animation context when moving the panel to ensure smooth transitions:
    if #available(iOS 18.0, *) {
        let animation = context.transaction.animation ?? .spring(response: 0.25, dampingFraction: 0.9)
        UIView.animate(animation) {
            proxy.move(to: .full, animated: false)
        }
    }
  7. Configure content scaling with contentMode

    master

    To control how the surface height behaves when the position changes, use the contentMode property. Set it to .fitToBounds if you want the surface height to adjust to the bounds of FloatingPanelController.view.

    Note: When using .fitToBounds, you are responsible for configuring Auto Layout constraints to prevent the content view's layout from breaking due to elastic surface height changes.

    fpc.contentMode = .fitToBounds
  8. Known limitation: UISearchController compatibility

    master
    Due to system design, UISearchController is not compatible with FloatingPanelController. When a user interacts with the search bar, UISearchController automatically presents itself modally and swaps the superview of the search bar to its own managed view. This prevents FloatingPanelController from controlling the search bar while it is active.
  9. Add a floating panel as a child view controller in UIKit

    master

    To add a floating panel as a child view controller in UIKit:

    1. Initialize a FloatingPanelController.
    2. Set its delegate (optional).
    3. Set the content view controller using .set(contentViewController:).
    4. Track a scroll view using .track(scrollView:).
    5. Add the panel to the parent view controller using .addPanel(toParent:).
    import UIKit
    import FloatingPanel
    
    class ViewController: UIViewController, FloatingPanelControllerDelegate {
        var fpc: FloatingPanelController!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Initialize a `FloatingPanelController` object.
            fpc = FloatingPanelController()
    
            // Assign self as the delegate of the controller.
            fpc.delegate = self // Optional
    
            // Set a content view controller.
            let contentVC = ContentViewController()
            fpc.set(contentViewController: contentVC)
    
            // Track a scroll view(or the siblings) in the content view controller.
            fpc.track(scrollView: contentVC.tableView)
    
            // Add and show the views managed by the `FloatingPanelController` object to self.view.
            fpc.addPanel(toParent: self)
        }
    }
  10. Display multiple floating panels in SwiftUI

    master

    You can display multiple floating panels within the same view hierarchy. To ensure each panel has its own unique layout or behavior, apply the specific modifiers (like .floatingPanelSurfaceAppearance) directly below the corresponding .floatingPanel call.

    Color.orange
        .ignoresSafeArea()
        .floatingPanel(
            coordinator: MyPanelCoordinator.self
        ) { proxy in
            ContentView(proxy: proxy)
        }
        .floatingPanelSurfaceAppearance(.transparent())
        .floatingPanel(
            coordinator: MyPanelCoordinator.self
        ) { proxy in
            ContentView(proxy: proxy)
        }
        .floatingPanelSurfaceAppearance(.transparent(cornerRadius: 24))