Click here
- iOS 15 or later
- Xcode 16 or later
Note
The SwiftUI API can be used on iOS 14, but it's out of the supported versions.
- Build SwiftUI APIs on top of our battle-tested UIKit implementation
- Enable smooth interoperability between UIKit and SwiftUI components
- Provide an idiomatic SwiftUI developer experience
- Complete reimplementation of FloatingPanel solely using SwiftUI APIs
- Cover all UIKit-specific customization options through SwiftUI APIs
- 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!
- 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 builds for iOS 14 or later, but has been primarily tested on iOS 15+
- We've determined that providing an integration API is the optimal approach with enhanced UIKit integration (as of iOS 18)
- Currently not using
@Entrydue to compatibility constraints
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.
The SwiftUI APIs provide a variety of view modifiers. Consider using these modifiers before implementing custom logic in your FloatingPanelCoordinator object.
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.
A FloatingPanelCoordinator implementation handles the following key responsibilities:
- Creation and Configuration: Initializing and configuring the underlying
FloatingPanelControllerinstance - View Hierarchy Management: Setting up the relationship between the main SwiftUI view and the panel content view
- State Management: Handling panel state transitions and position changes
- Event Handling: Capturing panel events and dispatching them back to SwiftUI
- Environment Changes: Responding to SwiftUI environment changes and updating the panel accordingly
The 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? = selfIn 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 {
...
}
}