Aiolos, ancient greek for quick-moving/nimble, is a Swift UI framework inspired by the floating panel, that was introduced to Maps app in iOS 11. Give it a try in MindNode 5 for iOS (free trial available).

It is fully gesture-driven, takes safe area insets into account, has support for right-to-left languages baked in and automatically reacts to the on-screen keyboard. Compared to many other open source panel solutions, Aiolos is designed to be an always-visible child view controller, and therefore does not use the custom view controller transition API of iOS.

Integration with Carthage

Add this line to your Cartfile.

github "IdeasOnCanvas/Aiolos"

Usage in Code

There's a demo app, that demonstrates how the Panel can be set up with a different configuration for iPhones and iPads.

func makePanel(with contentViewController: UIViewController) -> Panel {
    // create Panel with default configuration
    let configuration = Panel.Configuration.default
    let panelController = Panel(configuration: configuration)

    // specify, which ViewController is displayed in the panel
    panelController.contentViewController = contentViewController

    // setup delegates that handle size configuration and animation callbacks
    panelController.sizeDelegate = self
    panelController.animationDelegate = self

    // change the configuration to fit you needs
    panelController.configuration.position = self.panelPosition(for: self.traitCollection)
    panelController.configuration.margins = self.panelMargins(for: self.traitCollection)
    panelController.configuration.appearance.separatorColor = .white

    // we want a different look/behaviour on iPhone compared to iPad
    if self.traitCollection.userInterfaceIdiom == .pad {
        panelController.configuration.appearance.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
    } else {
        panelController.configuration.supportedModes = [.minimal, .compact, .expanded, .fullHeight]
        panelController.configuration.appearance.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]

    return panelController

Configuring the size

extension ViewController: PanelSizeDelegate {

    func panel(_ panel: Panel, sizeForMode mode: Panel.Configuration.Mode) -> CGSize {
        let width = self.panelWidth(for: self.traitCollection, position: panel.configuration.position)
        switch mode {
        case .minimal:
            return CGSize(width: width, height: 0.0)
        case .compact:
            return CGSize(width: width, height: 64.0)
        case .expanded:
            let height: CGFloat = self.traitCollection.userInterfaceIdiom == .phone ? 270.0 : 320.0
            return CGSize(width: width, height: height)
        case .fullHeight:
            return CGSize(width: width, height: 0.0)

Reacting to Panel animations

extension ViewController: PanelAnimationDelegate {

    func panel(_ panel: Panel, willTransitionTo size: CGSize) {
        print("Panel will transition to size \(size)")

    func panel(_ panel: Panel, willTransitionFrom oldMode: Panel.Configuration.Mode?, to newMode: Panel.Configuration.Mode, with coordinator: PanelTransitionCoordinator) {
        print("Panel will transition from \(oldMode) to \(newMode)")
        // we can animate things along the way
            print("Animating alongside of panel transition")
        }, completion: { animationPosition in
            print("Completed panel transition to \(newMode)")