# Technical Documentation: scenee/FloatingPanel

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/scenee/FloatingPanel)

### Overview (`README.md`)

# FloatingPanel

FloatingPanel is a simple and easy-to-use UI component designed for a user interface featured in Apple Maps, Shortcuts and Stocks app.
The user interface displays related content and utilities alongside the main content.






## Table of Contents

<details>
<summary>
Click here.
</summary>

- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Requirements](#requirements)
- [Documentation](#documentation)
- [Installation](#installation)
  - [Swift Package Manager](#swift-package-manager)
    - [Using Xcode](#using-xcode)
    - [Using Package.swift](#using-packageswift)
  - [CocoaPods](#cocoapods)
- [Getting Started with SwiftUI](#getting-started-with-swiftui)
  - [Adding a floating panel within a view](#adding-a-floating-panel-within-a-view)
  - [Presenting a floating panel modally within a view](#presenting-a-floating-panel-modally-within-a-view)
  - [Displaying multiple panels](#displaying-multiple-panels)
  - [Next step](#next-step)
- [Getting Started with UIKit](#getting-started-with-uikit)
  - [Adding a floating panel as a child view controller](#adding-a-floating-panel-as-a-child-view-controller)
  - [Presenting a floating panel modally](#presenting-a-floating-panel-modally)
  - [Next step](#next-step-1)
- [Maintainer](#maintainer)
- [License](#license)

</details>

## Features

- Simple container view controller
- Fluid behavior using numeric springing
- Scroll view tracking
- Removal interaction
- Multi panel support
- Modal presentation
- Support for 4 positions (top, left, bottom, right)
- 1 or more magnetic anchors(full, half, tip and more)
- Layout support for all trait environments(i.e. Landscape orientation)
- Common UI elements: surface, backdrop and grabber handle
- Free from common Auto Layout and gesture handling issues
- Compatible with Objective-C
- SwiftUI API support

Examples can be found here:

- [Maps](Examples/Maps) like Apple Maps.app.
- [Maps-SwiftUI](Examples/Maps-SwiftUI) like Apple Maps.app using SwiftUI.
- [Stocks](Examples/Stocks) like Apple Stocks.app.
- [Samples](Examples/Samples)
- [SamplesObjC](Examples/SamplesObjC)
- [SamplesSwiftUI](Examples/SamplesSwiftUI)

## Requirements

FloatingPanel is written in Swift 5.0+ and compatible with iOS 15.0+.

## Documentation

- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/3.2.4/documentation/floatingpanel)
- [FloatingPanel SwiftUI API Guide](/Documentation/FloatingPanel%20SwiftUI%20API%20Guide.md)
- [FloatingPanel API Guide](/Documentation/FloatingPanel%20API%20Guide.md)
- [FloatingPanel 2.0 Migration Guide](/Documentation/FloatingPanel%202.0%20Migration%20Guide.md)

## Installation

### Swift Package Manager

#### Using Xcode

Just follow [this documentation](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).

#### Using Package.swift

In your Package.swift Swift Package Manager manifest, add the following dependency to your dependencies argument:

```swift
.package(url: "https://github.com/scenee/FloatingPanel", from: "3.2.4"),
```

Add Numerics as a dependency for your target:

```swift
.target(name: "MyTarget", dependencies: [
  .product(name: "FloatingPanel", package: "FloatingPanel"),
  "AnotherModule"
]),
```

And then add `import FloatingPanel` in your source code.

### CocoaPods

FloatingPanel is available through [CocoaPods](https://cocoapods.org). To install
it, simply add the following line to your Podfile:

```ruby
pod 'FloatingPanel'
```

## Getting Started with SwiftUI

### Adding a floating panel within a view

```swift
@State private var layout = MyFloatingPanelLayout()
@State private var state: FloatingPanelState?

var view: some View {
  MainView()
      .floatingPanel { proxy in
          ScrollView {
              VStack(spacing: 20) {
                  ForEach(items) { item in
                      ItemRow(item)
                  }
              }
              .padding()
          }
          .floatingPanelScrollTracking(proxy: proxy)
      }
      .floatingPanelState($state)
      .floatingPanelLayout(layout)
      .floatingPanelBehavior(MyCustomBehavior())
      .floatingPanelSurfaceAppearance(.transparent)
}
```

### Presenting a floating panel modally within a view

Please define a custom coordinator object to present a floating panel as a modality.

```swift
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)
        }
    }
    ...
}
```

### Displaying multiple panels
  
Multiple floating panels can be displayed in the same view hierarchy. To customize the layout and behavior for each Floating Panel individually, place modifiers directly below each panel.

```swift
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))
```

### Next step

For more details, see the [FloatingPanel SwiftUI API Guide](/Documentation/FloatingPanel%20SwiftUI%20API%20Guide.md)

## Getting Started with UIKit

### Adding a floating panel as a child view controller

```swift
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)
    }
}
```

### Presenting a floating panel modally

```swift
let fpc = FloatingPanelController()
let contentVC = ...
fpc.set(contentViewController: contentVC)

fpc.isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-down

self.present(fpc, animated: true, completion: nil)
```

You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style.

> [!NOTE]
> FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [Transitioning](Sources/Transitioning.swift).

### Next step

For more details, see the [FloatingPanel API Guide](/Documentation/FloatingPanel%20API%20Guide.md).

## Maintainer

Shin Yamamoto <shin@scenee.com> | [@scenee](https://twitter.com/scenee)

## License

FloatingPanel is available under the MIT license. See the LICENSE file for more info.

[](https://www.star-history.com/?repos=scenee%2FFloatingPanel&type=date&legend=bottom-right)

---

### Overview (`Examples/SamplesSwiftUI/README.md`)

# SamplesSwiftUI

## Requirements

* iOS 15 or later
* Xcode 16 or later

---

### Documentation FloatingPanel 2.0 Migration Guide (`Documentation/FloatingPanel 2.0 Migration Guide.md`)

# FloatingPanel 2.0 Migration Guide

FloatingPanel 2.0 is the latest major release of FloatingPanel. As a major release, following Semantic Versioning conventions, 2.0 introduces API-breaking changes.

This guide is provided in order to ease the transition of existing applications using FloatingPanel 1.x to the latest APIs, as well as explain the design and structure of new and updated functionality.

## Updated Minimum Requirements

* Swift 5.0
* iOS 11 (iOS 10 is still the deployment target, but not tested well)
* Xcode 11.0

## Benefits of Upgrading

* __Top, left and right positioned panel__
  * FloatingPanel is not just a library for a bottom positioned panel, but also top, left and right positioned ones.
* __Objective-C compatibility__
  * The entire APIs are exposed in Objective-C. So you can use them in Objective-C directly.
* __Flexible and explicit layout customization__
  * `FloatingPanelLayout` is redesigned. There is no implicit rules to lay out a panel anymore.
* __New spring animation without UIViewPropertyAnimator__
  * The new spring animation uses [Numeric springing](http://allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/) which is a very powerful tool for procedural animation. Therefore a library consumer is easy to modify a panel behavior by 2 parameters of the deceleration rate and response time.
* __Handle the panel position anytime__
  * `floatingPanelDidMove(_:)` delegate method is also called while a panel is moving. The method behavior becomes same as `scrollViewDidScroll(_:)` in `UIScrollViewDelegate`. And in the method a library consumer is able to change a panel location.
* __Update the removal interaction's invocation__
  * Now you can invoke the removal interaction at any time where you want. There is no restrictions in the library.
* __Fix many issues depending on API design__
  * See the following sections for details.

## API Name Changes

* `FloatingPanelPosition` is now `FloatingPanelState`.
  * `FloatingPanelPosition` in v2 is used to specify a panel position(top, left, bottom and right) in a screen.
* `FloatingPanelSurfaceView` is `SurfaceView` only in Swift.
* `FloatingPanelBackdropView` is `BackdropView` only in Swift.
* `FloatingPanelGrabberHandleView` is `GrabberView` only in Swift.
* "decelerate" term is replaced with "attract" because the panel's behavior is not unidirectional, but going back and forth so that it is settled to a location.

## `FloatingPanelController`

* `layout` and `behavior` properties can be changed directly without using the delegate methods.

```swift
fpc.behavior = SearchPaneliPadBehavior()
fpc.layout = SearchPaneliPadLayout()
fpc.invalidateLayout() // If needed
```

* The second argument of `addPanel(toParent:)` changes to specify an index of subviews of a view in which a panel is added.

```diff
- public func addPanel(toParent parent: UIViewController, belowView: UIView? = nil, animated: Bool = false) {
+ public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false) {
```

* `surfaceOriginY` is now `surfaceLocation`.
* `updateLayout` is now `invalidateLayout`.
* The scroll tracking API is changed a bit to support multiple scroll view tracking in the future.
  * Now `untrack(scrollView:)` is used to disable the scroll tracking.

## `FloatingPanelControllerDelegate`

* `floatingPanelDidEndDragging(_ vc:willAttract:)` is added to check whether a panel will continue to move after dragging.
* `floatingPanelDidMove(_:)` behavior changes. The method is also called in the spring animation.
* The removal interaction delegate is updated.
  * `floatingPanel(_:shouldRemoveAt:with:)` is added to determine whether it invokes the removal interaction in any state.
  * `floatingPanelWillRemove(_:)` is added.
* `floatingPanel(_: FloatingPanelController, layoutFor size: CGSize)` is added to respond to a layout change in regular size classes on iPad.

```swift
func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout {
    if aCondition(for: size) {
        return SearchPanelLayout()
    }
    return SearchPanel2Layout()
}
```

* The `targetState` argument type of `floatingPanelWillEndDragging(_:withVelocity:targetState:)` is changed from `FloatingPanelState` to  `UnsafeMutablePointer<FloatingPanelState>` to modify a target state on demand.

```swift
func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>) {
    switch targetState.pointee {
        case .full:
          // do something...
        case .half:
            if aCondition {
                targetState.pointee = .tip
            }
        default:
            break
    }
}
```

### Deprecated APIs

* `floatingPanel(_:behaviorFor:)`
  * Please update `FloatingPanelController.behavior` directly.
* `floatingPanel(_:shouldRecognizeSimultaneouslyWith:)`
  * Please use `FloatingPanelController.panGestureRecognizer.delegateProxy`.

## `FloatingPanelLayout`

* `position` property is added to determine a panel position.
* `initialPosition` is now `initialState`.
* `supportedPositions` and `insetFor(position:)` are replaced with `anchors` property.
* `backdropAlphaFor(position:)` is now `backdropAlpha(for:)`.

```swift
class SearchPanelPadLayout: FloatingPanelLayout {
    let position: FloatingPanelPosition  = .top
    let initialState: FloatingPanelState = .tip
    var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
        return [
            ...
        ]
    }
    func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
        return 0.3
    }
    ...
}
```

### New `FloatingPanelLayoutAnchoring` classes

The following objects adopting `FloatingPanelLayoutAnchoring` protocol are added to configure the flexible and explicit layout.

#### `FloatingPanelLayoutAnchor`

This class is used to specify a panel layout using insets from a rectangle area of the superview or safe area.

* `FloatingPanelFullScreenLayout` is replaced with anchors using `.superview` reference guide.
* `FloatingPanelLayoutAnchor(fractionalInset:edge:referenceGuide:)` lets you lay out a panel at a relative position in a reference rectangle area.

```swift
// Before:
class MyPanelLayout: FloatingPanelLayout {
    var initialPosition: FloatingPanelPosition {
        return .half
    }

    func insetFor(position: FloatingPanelPosition) -> CGFloat? {
        switch position {
        case .full: return 16.0
        case .half: return 262.0
        case .tip: return 44.0
        case .hidden: return nil
        }
    }
}

// After:
class MyPanelLayout: FloatingPanelLayout {
    var position: FloatingPanelPosition = .bottom
    var initialState: FloatingPanelState { .half }
    var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
        return [
            .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
            .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
            .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
        ]
    }
}
```

#### `FloatingPanelIntrinsicLayoutAnchor`

This class is used to specify a panel layout using offsets from the intrinsic size layout.

* This replaces `FloatingPanelIntrinsicLayout`.
* This is also able to configure a fractional layout in the intrinsic size.

```swift
// Before:
class MyPanelIntrinsicLayout: FloatingPanelIntrinsicLayout {
    var initialPosition: FloatingPanelPosition {
        return .half
    }
    func insetFor(position: FloatingPanelPosition) -> CGFloat? {
        switch position {
        case .full: return 16.0
        case .half: return 262.0
        case .tip: return 44.0
        case .hidden: return nil
        }
    }
}

// After:
class MyPanelIntrinsicLayout: FloatingPanelLayout {
    var position: FloatingPanelPosition = .bottom
    var initialState: FloatingPanelState { .full }
    var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
        return [
            .full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 16.0, referenceGuide: .safeArea),
            .half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
            .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
        ]
    }
}
```

### Deprecated APIs

* `.topInteractionBuffer` and `.bottomInteractionBuffer`.
  * Please control the max/min range of the motion in `floatingPanelDidMove(_:)` delegate method as below.

```swift
func floatingPanelDidMove(_ fpc: FloatingPanelController) {
    if fpc.isAttracting == false {
        let loc = fpc.surfaceLocation
        let minY = fpc.surfaceLocation(for: .full).y - 6.0
        let maxY = fpc.surfaceLocation(for: .tip).y + 6.0
        fpc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY))
    }
}
```

## `FloatingPanelBehavior`

* `.springDecelerationRate` and `.springResponseTime` properties are added to control the new spring effect of Numeric springing.

### Deprecated APIs

* `addAnimator(_:to:)`, `removeAnimator(_:from:)`
  * They are moved into `floatingPanel(_:animatorForPresentingTo:)` and  `floatingPanel(_:animatorForDismissingWith:)` of `FloatingPanelControllerDelegate` because they are used for view transitions.
* `interactionAnimator(_:to:with:)`, `moveAnimator(_:from:to:)`
  * They are removed because the animators are replaced with the new spring effect.
* `removalVelocity`, `removalProgress`
  * They are replaced with `floatingPanel(_:shouldRemoveAt:with:)` of `FloatingPanelControllerDelegate`
* `removalInteractionAnimator(_:with:)`
  * It is integrated with `floatingPanel(_:animatorForDismissingWith:)` of `FloatingPanelControllerDelegate`.

## `SurfaceView`

* `SurfaceAppearance` class and `SurfaceView.appearance` property are added to specify the rounding corners, shadows and background color.
  * `SurfaceView.appearance` property avoids `Ambiguous use of 'cornerRadius'` error, for instance.
  * `SurfaceAppearance` enables to apply layered box shadows into a surface to materialize it.

```swift
// Before:
fpc.surfaceView.cornerRadius = 6.0
fpc.surfaceView.backgroundColor = .clear
fpc.surfaceView.shadowHidden = false
fpc.surfaceView.shadowColor = .black
fpc.surfaceView.shadowOffset = CGSize(width: 0, height: 16)
fpc.surfaceView.shadowRadius = 16.0

// After:
let appearance = SurfaceAppearance()
appearance.cornerRadius = 8.0
appearance.backgroundColor = .clear

let shadow = SurfaceAppearance.Shadow()
shadow.color = .black
shadow.offset = CGSize(width: 0, height: 16)
shadow.radius = 16
shadow.spread = 8
appearance.shadows = [shadow]

fpc.surfaceView.appearance = appearance
```

* These properties are changed for the top, left and right positioned panel.
  * `grabberTopPadding` is now `grabberHandlePadding`.
  * `topGrabberBarHeight` is now `grabberAreaOffset`.
  * `grabberHandleWidth` and `grabberHandleHeight` are replaced with `grabberHandleSize`.

## `BackdropView`

* The dismissal action of the backdrop is disabled by default.
  * You can enable it to set `BackdropView.dismissalTapGestureRecognizer.isEnabled` to `true`.

## `FloatingPanelPanGestureRecognizer`

* `delegateProxy` property is added to intercept the gesture recognizer delegate.

```swift
func layoutPanelForPad() {
    fpc.behavior = SearchPaneliPadBehavior()
    fpc.panGestureRecognizer.delegateProxy = self
}

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return false
}
```

## Miscellaneous

* `UISpringTimingParameters(decelerationRate:frequencyResponse:initialVelocity:)` initializer is added.
* The directory structure and file names in the Xcode project changes.

---

### Documentation FloatingPanel API Guide (`Documentation/FloatingPanel API Guide.md`)

# Floating Panel API Guide

## Table of Contents

<details>
<summary>Click here</summary>

- [Table of Contents](#table-of-contents)
- [View hierarchy](#view-hierarchy)
- [Usage](#usage)
  - [Show/Hiding a floating panel in a view with your view hierarchy](#showhiding-a-floating-panel-in-a-view-with-your-view-hierarchy)
  - [Scaling the content view when the surface position changes](#scaling-the-content-view-when-the-surface-position-changes)
  - [Customizing the layout with `FloatingPanelLayout` protocol](#customizing-the-layout-with-floatingpanellayout-protocol)
    - [Changing the initial layout](#changing-the-initial-layout)
  - [Updating your panel layout](#updating-your-panel-layout)
    - [Supporting your landscape layout](#supporting-your-landscape-layout)
    - [Using the intrinsic size of a content in your panel layout](#using-the-intrinsic-size-of-a-content-in-your-panel-layout)
    - [Specifying an anchor for each state by an inset of the `FloatingPanelController.view` frame](#specifying-an-anchor-for-each-state-by-an-inset-of-the-floatingpanelcontrollerview-frame)
    - [Changing the backdrop alpha](#changing-the-backdrop-alpha)
    - [Using custom panel states](#using-custom-panel-states)
  - [Customizing the behavior with `FloatingPanelBehavior` protocol](#customizing-the-behavior-with-floatingpanelbehavior-protocol)
    - [Modifying your floating panel's interaction](#modifying-your-floating-panels-interaction)
    - [Activating the rubber-band effect on panel edges](#activating-the-rubber-band-effect-on-panel-edges)
    - [Managing the projection of a pan gesture momentum](#managing-the-projection-of-a-pan-gesture-momentum)
  - [Specifying the panel move's boundary](#specifying-the-panel-moves-boundary)
  - [Customizing the surface design](#customizing-the-surface-design)
    - [Modifying your surface appearance](#modifying-your-surface-appearance)
    - [Use a custom grabber handle](#use-a-custom-grabber-handle)
    - [Customizing layout of the grabber handle](#customizing-layout-of-the-grabber-handle)
    - [Customizing content padding from surface edges](#customizing-content-padding-from-surface-edges)
    - [Customizing margins of the surface edges](#customizing-margins-of-the-surface-edges)
  - [Customizing gestures](#customizing-gestures)
    - [Suppressing the panel interaction](#suppressing-the-panel-interaction)
    - [Adding tap gestures to the surface view](#adding-tap-gestures-to-the-surface-view)
    - [Interrupting the delegate methods of `FloatingPanelController.panGestureRecognizer`](#interrupting-the-delegate-methods-of-floatingpanelcontrollerpangesturerecognizer)
  - [Creating an additional floating panel for a detail](#creating-an-additional-floating-panel-for-a-detail)
  - [Moving a position with an animation](#moving-a-position-with-an-animation)
  - [Working your contents together with a floating panel behavior](#working-your-contents-together-with-a-floating-panel-behavior)
  - [Enabling the tap-to-dismiss action of the backdrop view](#enabling-the-tap-to-dismiss-action-of-the-backdrop-view)
  - [Allowing to scroll content of the tracking scroll view in addition to the most expanded state](#allowing-to-scroll-content-of-the-tracking-scroll-view-in-addition-to-the-most-expanded-state)
- [Notes](#notes)
  - ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
  - [UISearchController issue](#uisearchcontroller-issue)

</details>

## View hierarchy

`FloatingPanelController` manages the views as the following view hierarchy.

```text
FloatingPanelController.view (FloatingPanelPassThroughView)
 ├─ .backdropView (FloatingPanelBackdropView)
 └─ .surfaceView (FloatingPanelSurfaceView)
    ├─ .containerView (UIView)
    │  └─ .contentView (FloatingPanelController.contentViewController.view)
    └─ .grabber (FloatingPanelGrabberView)
```

## Usage

### Show/Hiding a floating panel in a view with your view hierarchy

If you need more control over showing and hiding the floating panel, you can forgo the `addPanel` and `removePanelFromParent` methods. These methods are a convenience wrapper for **FloatingPanel**'s `show` and `hide` methods along with some required setup.

There are two ways to work with the `FloatingPanelController`:

1. Add it to the hierarchy once and then call `show` and `hide` methods to make it appear/disappear.
2. Add it to the hierarchy when needed and remove afterwards.

The following example shows how to add the controller to your `UIViewController` and how to remove it. Make sure that you never add the same `FloatingPanelController` to the hierarchy before removing it.

**NOTE**: `self.` prefix is not required, nor recommended. It's used here to make it clearer where do the functions used come from. `self` is an instance of a custom UIViewController in your code.

```swift
// 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.
// Constraint the fpc.view to all four edges of your controller's view.
// It makes the layout more robust on trait collection change.
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)
}
```

After you add the `FloatingPanelController` as seen above, you can call `fpc.show(animated: true) { }` to show the panel and `fpc.hide(animated: true) { }` to hide it.

To remove the `FloatingPanelController` from the hierarchy, follow the example below.

```swift
// Inform the panel controller that it will be removed from the hierarchy.
fpc.willMove(toParent: nil)
    
// Hide the floating panel.
fpc.hide(animated: true) {
    // Remove the floating panel view from your controller's view.
    fpc.view.removeFromSuperview()
    // Remove the floating panel controller from the controller hierarchy.
    fpc.removeFromParent()
}
```

### Scaling the content view when the surface position changes

Specify the `contentMode` to `.fitToBounds` if the surface height fits the bounds of `FloatingPanelController.view` when the surface position changes

```swift
fpc.contentMode = .fitToBounds
```

Otherwise, `FloatingPanelController` fixes the content by the height of the top most position.

> [!NOTE]
> In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.

### Customizing the layout with `FloatingPanelLayout` protocol

#### Changing the initial layout

```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
    ... {
        fpc = FloatingPanelController(delegate: self)
        fpc.layout = MyFloatingPanelLayout()
    }
}

class MyFloatingPanelLayout: FloatingPanelLayout {
    let position: FloatingPanelPosition = .bottom
    let initialState: FloatingPanelState = .tip
    let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
        .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
        .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
        .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea),
    ]
}
```

### Updating your panel layout

There are 2 ways to update the panel layout.

1. Manually set `FloatingPanelController.layout` to the new layout object directly.

```swift
fpc.layout = MyPanelLayout()
fpc.invalidateLayout() // If needed
```

Note: If you already set the `delegate` property of your `FloatingPanelController` instance, `invalidateLayout()` overrides the layout object of `FloatingPanelController` with one returned by the delegate object.

2. Returns an appropriate layout object in one of 2 `floatingPanel(_:layoutFor:)` delegates.

```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
    ...
    func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
        return MyFloatingPanelLayout()
    }

    // OR
    func floatingPanel(_ vc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout {
        return MyFloatingPanelLayout()
    } 
}
```

#### Supporting your landscape layout

```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
    ...
    func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
        return (newCollection.verticalSizeClass == .compact) ? LandscapePanelLayout() : FloatingPanelBottomLayout()
    }
}

class LandscapePanelLayout: FloatingPanelLayout {
    let position: FloatingPanelPosition = .bottom
    let initialState: FloatingPanelState = .tip
    let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
        .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
        .tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
    ]
    
    func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
        return [
            surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
            surfaceView.widthAnchor.constraint(equalToConstant: 291),
        ]
    }
}
```

#### Using the intrinsic size of a content in your panel layout

1. Lay out your content View with the intrinsic height size. For example, see "Detail View Controller scene"/"Intrinsic View Controller scene" of [Main.storyboard](Examples/Samples/Sources/Base.lproj/Main.storyboard). The 'Stack View.bottom' constraint determines the intrinsic height.
2. Specify layout anchors using `FloatingPanelIntrinsicLayoutAnchor`.

```swift
class IntrinsicPanelLayout: FloatingPanelLayout {
    let position: FloatingPanelPosition = .bottom
    let initialState: FloatingPanelState = .full
    let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
        .full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea),
        .half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
    ]
    ...
}
```

> [!WARNING]
> `FloatingPanelIntrinsicLayout` is deprecated on v1.

#### Specifying an anchor for each state by an inset of the `FloatingPanelController.view` frame

Use `.superview` reference guide in your anchors.

```swift
class MyFullScreenLayout: FloatingPanelLayout {
    ...
    let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
        .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .superview),
        .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
        .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .superview),
    ]
}
```

> [!WARNING]
> `FloatingPanelFullScreenLayout` is deprecated on v1.

#### Changing the backdrop alpha

You can change the backdrop alpha by `FloatingPanelLayout.backdropAlpha(for:)` for each state(`.full`, `.half` and `.tip`).

For instance, if a panel seems like the backdrop view isn't there on `.half` state, it's time to implement the backdropAlpha API and return a value for the state as below.

```swift
class MyPanelLayout: FloatingPanelLayout {
    func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
        switch state {
        case .full, .half: return 0.3
        default: return 0.0
        }
    }
}
```

#### Using custom panel states

You're able to define custom panel states and use them as the following example.

```swift
extension FloatingPanelState {
    static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750)
    static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250)
}

class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout {
    override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
        return [
            .full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
            .lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea),
            .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
            .firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea),
            .tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea),
        ]
    }
}
```

### Customizing the behavior with `FloatingPanelBehavior` protocol

#### Modifying your floating panel's interaction

```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
    ...
    func viewDidLoad() {
        ...
        fpc.behavior =  CustomPanelBehavior()
    }
}

class CustomPanelBehavior: FloatingPanelBehavior {
    let springDecelerationRate = UIScrollView.DecelerationRate.fast.rawValue + 0.02
    let springResponseTime = 0.4
    func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool {
        return true
    }
}
```

> [!WARNING]
> `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.

#### Activating the rubber-band effect on panel edges

```swift
class MyPanelBehavior: FloatingPanelBehavior {
    ...
    func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
        return true
    }
}
```

#### Managing the projection of a pan gesture momentum

This allows full projectional panel behavior. For example, a user can swipe up a panel from tip to full nearby the tip position.

```swift
class MyPanelBehavior: FloatingPanelBehavior {
    ...
    func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool {
        return true
    }
}
```

### Specifying the panel move's boundary

`FloatingPanelController.surfaceLocation` in `floatingPanelDidMove(_:)` delegate method behaves like `UIScrollView.contentOffset` in `scrollViewDidScroll(_:)`.
As a result, you can specify the boundary of a panel move as below.

```swift
func floatingPanelDidMove(_ vc: FloatingPanelController) {
    if vc.isAttracting == false {
        let loc = vc.surfaceLocation
        let minY = vc.surfaceLocation(for: .full).y - 6.0
        let maxY = vc.surfaceLocation(for: .tip).y + 6.0
        vc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY))
    }
}
```

> [!WARNING]
> `{top,bottom}InteractionBuffer` property is removed from `FloatingPanelLayout` since v2.

### Customizing the surface design

#### Modifying your surface appearance

```swift
// Create a new appearance.
let appearance = SurfaceAppearance()

// Define shadows
let shadow = SurfaceAppearance.Shadow()
shadow.color = UIColor.black
shadow.offset = CGSize(width: 0, height: 16)
shadow.radius = 16
shadow.spread = 8
appearance.shadows = [shadow]

// Define corner radius and background color
appearance.cornerRadius = 8.0
appearance.backgroundColor = .clear

// Set the new appearance
fpc.surfaceView.appearance = appearance
````

#### Use a custom grabber handle

```swift
let myGrabberHandleView = MyGrabberHandleView()
fpc.surfaceView.grabberHandle.isHidden = true
fpc.surfaceView.addSubview(myGrabberHandleView)
```

#### Customizing layout of the grabber handle

```swift
fpc.surfaceView.grabberHandlePadding = 10.0
fpc.surfaceView.grabberHandleSize = .init(width: 44.0, height: 12.0)
```

> [!NOTE]
> `grabberHandleSize` width and height are reversed in the left/right position.

#### Customizing content padding from surface edges

```swift
fpc.surfaceView.contentPadding = .init(top: 20, left: 20, bottom: 20, right: 20)
```

#### Customizing margins of the surface edges

```swift
fpc.surfaceView.containerMargins = .init(top: 20.0, left: 16.0, bottom: 16.0, right: 16.0)
```

The feature can be used for these 2 kind panels

- Facebook/Slack-like panel whose surface top edge is separated from the grabber handle.
- iOS native panel to display AirPods information, for example.

### Customizing gestures

#### Suppressing the panel interaction

You can disable the pan gesture recognizer directly

```swift
fpc.panGestureRecognizer.isEnabled = false
```

Or use this `FloatingPanelControllerDelegate` method.

```swift
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool {
    return aCondition ?  false : true
}
```

#### Adding tap gestures to the surface view

```swift
override func viewDidLoad() {
    ...
    let surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
    fpc.surfaceView.addGestureRecognizer(surfaceTapGesture)
    surfaceTapGesture.isEnabled = (fpc.position == .tip)
}

// Enable `surfaceTapGesture` only at `tip` state
func floatingPanelDidChangeState(_ vc: FloatingPanelController) {
    surfaceTapGesture.isEnabled = (vc.position == .tip)
}
```

#### Interrupting the delegate methods of `FloatingPanelController.panGestureRecognizer`

If you are set `FloatingPanelController.panGestureRecognizer.delegateProxy` to an object adopting `UIGestureRecognizerDelegate`, it overrides delegate methods of the pan gesture recognizer.

```swift
class MyGestureRecognizerDelegate: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return false
    }
}

class ViewController: UIViewController {
    let myGestureDelegate = MyGestureRecognizerDelegate()

    func setUpFpc() {
        ....
        fpc.panGestureRecognizer.delegateProxy = myGestureDelegate
    }
```

### Creating an additional floating panel for a detail

```swift
override func viewDidLoad() {
    // Setup Search panel
    self.searchPanelVC = FloatingPanelController()

    let searchVC = SearchViewController()
    self.searchPanelVC.set(contentViewController: searchVC)
    self.searchPanelVC.track(scrollView: contentVC.tableView)

    self.searchPanelVC.addPanel(toParent: self)

    // Setup Detail panel
    self.detailPanelVC = FloatingPanelController()

    let contentVC = ContentViewController()
    self.detailPanelVC.set(contentViewController: contentVC)
    self.detailPanelVC.track(scrollView: contentVC.scrollView)

    self.detailPanelVC.addPanel(toParent: self)
}
```

### Moving a position with an animation

In the following example, I move a floating panel to full or half position while opening or closing a search bar like Apple Maps.

```swift
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
    ...
    fpc.move(to: .half, animated: true)
}

func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
    ...
    fpc.move(to: .full, animated: true)
}
```

You can also use a view animation to move a panel.

```swift
UIView.animate(withDuration: 0.25) {
    self.fpc.move(to: .half, animated: false)
}
```

### Working your contents together with a floating panel behavior

```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
    ...
    func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
        if vc.position == .full {
            searchVC.searchBar.showsCancelButton = false
            searchVC.searchBar.resignFirstResponder()
        }
    }

    func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>) {
        if targetState.pointee != .full {
            searchVC.hideHeader()
        }
    }
}
```

### Enabling the tap-to-dismiss action of the backdrop view

The tap-to-dismiss action is disabled by default. So it needs to be enabled as below.

```swift
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
```

### Allowing to scroll content of the tracking scroll view in addition to the most expanded state

Just define conditions to allow content scrolling in `floatingPanel(:_:shouldAllowToScroll:in)` delegate method. If the returned value is true, the scroll content scrolls when its scroll position is not at the top of the content.

```swift
class MyViewController: FloatingPanelControllerDelegate {
    ... 

    func floatingPanel(
        _ fpc: FloatingPanelController,
        shouldAllowToScroll trackingScrollView: UIScrollView,
        in state: FloatingPanelState
    ) -> Bool {
        return state == .full || state == .half
    }
}
```

## Notes

### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller

'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC(except for modality).

`FloatingPanelController` has no way to manage a stack of view controllers like `UINavigationController`. If so, it would be so complicated and the interface will become `UINavigationController`. This component should not have the responsibility to manage the stack.

By the way, a content view controller can present a view controller modally with `present(_:animated:completion:)` or 'Present Modally' segue.

However, sometimes you want to show a destination view controller of 'Show' or 'Show Detail' segue with another floating panel. It's possible to override `show(_:sender)` of the master VC!

Here is an example.

```swift
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)
    }
}
```

A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondary floating panel set the destination view controller to the content.

It's a great way to decouple between a floating panel and the content VC.

### UISearchController issue

`UISearchController` isn't able to be used with `FloatingPanelController` by the system design.

Because `UISearchController` automatically presents itself modally when a user interacts with the search bar, and then it swaps the superview of the search bar to the view managed by itself while it displays. As a result, `FloatingPanelController` can't control the search bar when it's active, as you can see from [the screen shot](https://github.com/SCENEE/FloatingPanel/issues/248#issuecomment-521263831).

---

### Documentation FloatingPanel SwiftUI API Guide (`Documentation/FloatingPanel SwiftUI API Guide.md`)

# FloatingPanel SwiftUI API Guide

## Table of Contents

<details>
<summary>
Click here
</summary>

- [Table of Contents](#table-of-contents)
- [Requirements](#requirements)
- [Goals](#goals)
- [Non-Goals](#non-goals)
- [Design Principles](#design-principles)
- [Development and compatibility](#development-and-compatibility)
- [API implementation considerations](#api-implementation-considerations)
- [Supplemental view approach rather than modal presentation](#supplemental-view-approach-rather-than-modal-presentation)
- [Leveraging the view modifiers](#leveraging-the-view-modifiers)
- [`FloatingPanelCoordinator`: The key component](#floatingpanelcoordinator-the-key-component)
  - [Core Responsibilities](#core-responsibilities)
  - [Default Implementation](#default-implementation)
  - [Use Cases](#use-cases)
    - [1. Custom Event Handling](#1-custom-event-handling)
    - [2. Responding to Environment Changes](#2-responding-to-environment-changes)
    - [3. Modal Presentation](#3-modal-presentation)
  - [Best Practices](#best-practices)
    - [Define Meaningful Events](#define-meaningful-events)
    - [Use Lazy Delegate Initialization](#use-lazy-delegate-initialization)
    - [Handle Environment Changes Efficiently](#handle-environment-changes-efficiently)
    - [Coordinate with SwiftUI Animations](#coordinate-with-swiftui-animations)

</details>

## Requirements

- iOS 15 or later
- Xcode 16 or later

## Goals

1. Build SwiftUI APIs on top of our battle-tested UIKit implementation
2. Enable smooth interoperability between UIKit and SwiftUI components
3. Provide an idiomatic SwiftUI developer experience

## Non-Goals

1. Complete reimplementation of FloatingPanel solely using SwiftUI APIs
2. Cover all UIKit-specific customization options through SwiftUI APIs

## Design Principles

- APIs designed to maximize user control and flexibility
- FloatingPanel designed as a supplemental view rather than a modal presentation as the same as UIKit implementation.
- Declarative modifiers that follow SwiftUI conventions
- Environment-based configuration patterns
- Seamless integration with SwiftUI's view hierarchy
- Implementation of essential APIs only, with plans to expand based on user requests -- we welcome your feedback!

## Development and compatibility

- Built targeting Xcode 16 as the primary development environment
  - Maintains backward compatibility with Xcode 14/15 for UIKit support but gradually migrates to full Xcode 16+ features
- iOS version compatibility:
  - Our SwiftUI integration requires iOS 15 or later, the same as the library's minimum deployment target

## API implementation considerations

- We've determined that providing an integration API is the optimal approach with enhanced UIKit integration (as of iOS 18)
- Currently not using `@Entry` due to compatibility constraints

## Supplemental view approach rather than modal presentation

FloatingPanel has been designed as a supplemental view rather than a modal presentation since its first release in the UIKit implementation. The SwiftUI APIs follow this same principle, allowing users to leverage this library for use cases not covered by Apple's built-in APIs.

For instance, the SwiftUI API deliberately doesn't provide an `isPresented` binding argument in the `floatingPanel(coordinator:onEvent:content:)` modifier. If you want to hide a floating panel, use the `.hidden` anchor state instead, which enables you to hide a floating panel while keeping the content pre-rendered outside the visible screen area.

## Leveraging the view modifiers

The SwiftUI APIs provide a variety of view modifiers. Consider using these modifiers before implementing custom logic in your `FloatingPanelCoordinator` object.

## `FloatingPanelCoordinator`: The key component

The `FloatingPanelCoordinator` protocol is a key component in the FloatingPanel's SwiftUI integration, serving as the bridge between SwiftUI's declarative UI framework and FloatingPanel's UIKit-based implementation.

It manages the connection between the SwiftUI view hierarchy and the underlying `FloatingPanelController`, handling setup, configuration, and event dispatching for floating panels within SwiftUI applications.

This secion explains the `FloatingPanelCoordinator` protocol in detail, including its purpose, implementation patterns, and common use cases to help you effectively integrate floating panels into your SwiftUI applications.

### Core Responsibilities

A `FloatingPanelCoordinator` implementation handles the following key responsibilities:

1. **Creation and Configuration**: Initializing and configuring the underlying `FloatingPanelController` instance
2. **View Hierarchy Management**: Setting up the relationship between the main SwiftUI view and the panel content view
3. **State Management**: Handling panel state transitions and position changes
4. **Event Handling**: Capturing panel events and dispatching them back to SwiftUI
5. **Environment Changes**: Responding to SwiftUI environment changes and updating the panel accordingly

### Default Implementation

The library provides [`FloatingPanelDefaultCoordinator`](Sources/SwiftUI/FloatingPanelCoordinator.swift) as a standard implementation for basic panel integration.

`FloatingPanelCoordinator` intentionally does not provide default implementations for its required methods. This design choice avoids implicit behavior when handling the `FloatingPanelController`. When implementing a custom coordinator instead of using `FloatingPanelDefaultCoordinator`, users can clearly understand the requirements of the `FloatingPanelCoordinator` protocol and how the `FloatingPanelController` should be managed in their custom implementation.

### Use Cases

#### 1. Custom Event Handling

Define a custom coordinator to handle panel events:

```swift
struct ContentView: View {
    var body: some View {
        Color.blue
            .ignoresSafeArea()
            .floatingPanel(
                coordinator: MyPanelCoordinator.self,
                onEvent: handlePanelEvent
            ) { proxy in
                PanelContent(proxy: proxy)
            }
    }
    
    func handlePanelEvent(_ event: MyPanelCoordinator.Event) {
        switch event {
        case .willChangeState(let state):
            print("Panel will change to \(state)")
        case .didChangeState(let state):
            print("Panel changed to \(state)")
        }
    }
}

class MyPanelCoordinator: FloatingPanelCoordinator {
    enum Event {
        case willChangeState(FloatingPanelState)
        case didChangeState(FloatingPanelState)
    }
    
    let action: (Event) -> Void
    lazy var delegate: FloatingPanelControllerDelegate? = self
    let proxy: FloatingPanelProxy
    
    ...
}

extension MyPanelCoordinator: FloatingPanelControllerDelegate {
    func floatingPanelWillBeginDragging(_ fpc: FloatingPanelController) {
        action(.willChangeState(fpc.state))
    }
    
    func floatingPanelDidChangeState(_ fpc: FloatingPanelController) {
        action(.didChangeState(fpc.state))
    }
}
```

#### 2. Responding to Environment Changes

Create a coordinator that responds to SwiftUI environment changes:

```swift
class EnvironmentAwarePanelCoordinator: FloatingPanelCoordinator {
    ...
    func onUpdate<Representable>(
        context: UIViewControllerRepresentableContext<Representable>
    ) where Representable: UIViewControllerRepresentable {
        // Access environment values and update the panel
        let shouldMoveToFullState = context.environment.someCustomValue
        if shouldMoveToFullState {
            proxy.move(to: .full, animated: true)
        }
    }
}
```

#### 3. Modal Presentation

Implement a coordinator that presents the panel modally:

```swift
class ModalPanelCoordinator: FloatingPanelCoordinator {
    enum Event {
        case dismissed
    }
    
    let action: (Event) -> Void
    let proxy: FloatingPanelProxy
    lazy var delegate: FloatingPanelControllerDelegate? = nil
    
    ...
    func setupFloatingPanel<Main, Content>(
        mainHostingController: UIHostingController<Main>,
        contentHostingController: UIHostingController<Content>
    ) where Main: View, Content: View {
        // Set up the content
        contentHostingController.view.backgroundColor = .clear
        controller.set(contentViewController: contentHostingController)
        
        // Present the panel modally
        Task { @MainActor in
            controller.isRemovalInteractionEnabled = true
            controller.delegate = self
            mainHostingController.present(controller, animated: true)
        }
    }
    
    ...
}

extension ModalPanelCoordinator: FloatingPanelControllerDelegate {
    func floatingPanelDidEndRemove(_ fpc: FloatingPanelController) {
        action(.dismissed)
    }
}
```

### Best Practices

#### Define Meaningful Events

Design your `Event` type to capture meaningful panel interactions that SwiftUI views need to respond to, but avoid over-engineering with too many event types.

#### Use Lazy Delegate Initialization

Initialize `delegate` lazily when you want your coordinator to implement `FloatingPanelControllerDelegate`:

```swift
lazy var delegate: FloatingPanelControllerDelegate? = self
```

#### Handle Environment Changes Efficiently

In your `onUpdate` method, compare environment values with current panel state before making changes to avoid unnecessary updates.

#### Coordinate with SwiftUI Animations

Respect SwiftUI's animation context when moving the panel on iOS 18 or later:

```swift
func onUpdate<Representable>(
    context: UIViewControllerRepresentableContext<Representable>
) where Representable: UIViewControllerRepresentable {
    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)
        }
    } else {
        ...
    }
}
```

---

--- METRICS ---
- Files Extracted: 5
- Estimated Token Budget: ~13551 tokens
- Canonical Reference: https://codewiki.google/github.com/scenee/FloatingPanel
