Sources/FloatingPanel.docc/FloatingPanel SwiftUI API Guide.md
FloatingPanelCoordinator: The key component
[!NOTE] The SwiftUI API can be used on iOS 14, but it's out of the supported versions.
@Entry due to compatibility constraintsFloatingPanel 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.
The SwiftUI APIs provide a variety of view modifiers. Consider using these modifiers before implementing custom logic in your FloatingPanelCoordinator object.
FloatingPanelCoordinator: The key componentThe 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.
A FloatingPanelCoordinator implementation handles the following key responsibilities:
FloatingPanelController instanceThe library provides FloatingPanelDefaultCoordinator 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.
Define a custom coordinator to handle panel events:
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))
}
}
Create a coordinator that responds to SwiftUI environment changes:
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)
}
}
}
Implement a coordinator that presents the panel modally:
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)
}
}
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.
Initialize delegate lazily when you want your coordinator to implement FloatingPanelControllerDelegate:
lazy var delegate: FloatingPanelControllerDelegate? = self
In your onUpdate method, compare environment values with current panel state before making changes to avoid unnecessary updates.
Respect SwiftUI's animation context when moving the panel on iOS 18 or later:
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 {
...
}
}