🚂 Transition

Transition for Swift is a sufficient and super convenient solution for managing navigation within your application. You don’t need to “wrap” controllers, or use “base” classes or coordinators, and etc.

Using the library comes down to creating the Router and Route classes and calling the desired method from your UIViewController. You don’t even have to expand the controller itself with new protocols.

Here’s how it works:

🌱 Getting started

// 1️⃣ For UIViewController you create a router that will
// call the screen with the correct way to open
class HomeRouter: Router<HomeViewController> {}

// 2️⃣ Router should be in your UIViewController and be
// initialized at the time of the call
class HomeViewController: UIViewController {
    var router: ProfileRouter
    init(router: ProfileRouter) {
        self.router = router

// 3️⃣ At the same time, each module must have its own route,
// which will describe the methods in which the new screen will be initialized
protocol ProfileRoute {
    func presentProfile()
    func pushProfile()

extension ProfileRoute where Self: RouterProtocol {
    // This is how the function to show the Profile in the modal window would look like
    func presentProfile() {
        // Use one of the preset opening schemes or implement your own.
        let transition = ModalTransition()
        // Transition needs to be passed to the router, which is stored in your controller
        // It is convenient to do this in a class that assembles a module,
        // such as Assembly in VIPER
        let module = ProfileModule(transition: transition)
        // The Transition library method is called into which the opened UIViewController
        // will be passed and opening method
        open(module.view, transition: transition)
    // This is how the function to push Profile in current Navigation stack
    func pushProfile() {
        let transition = PushTransition()
        let module = ProfileModule(transition: transition)
        open(module.view, transition: transition)

// 4️⃣ And... that's all - you can call the router methods in your `UIViewController`
// in a convenient way for you, like this:
    UIAction(handler: { [weak self] _ in
    for: .touchUpInside

🌠 Demo

For a better understanding of how to work with Transition, please check out the demo project.

🦖 For more

  • The module’s native “push” and “present” are already defined in PushTransition and ModalTransition, just pass an instance of them to the open(_ : UIViewController, : Transition) method.

  • In addition, there is EmbedTransition which replaces the current controller with a new one using the didMove(toParent _: UIViewController?) method

  • For a modal view, you can pass configuration in the BottomSheetProps object. It is supported to change all (or at least the most important) settings that will ensure the correct behavior of the screen.

// Each property replicates native settings in UIKit, you shouldn't have
// not understanding how it works. Just in case, for the BottomSheetProps properties,
// their documentation is duplicated
let transition = ModalTransition(
    isAnimated: true,
    isNeedToEmbedInNavigationController: false,
    modalPresentationStyle: .pageSheet,
    bottomSheetProps: BottomSheetProps(
        doNotCloseOnDrag: false,
        detents: [
        selectedDetentIdentifier: .medium,
        prefersScrollingExpandsWhenScrolledToEdge: true,
        largestUndimmedDetentIdentifier: .medium,
        preferredCornerRadius: 32,
        prefersGrabberVisible: true
  • If your project already uses PanModal, FloatingPanel or any other screen opening animator, you can use it in conjunction with Transition. Just implement your own version of the Transition protocol. Like that:

import FloatingPanel
import UIKit

final class ModalFloatingPanelTransition: NSObject {
    weak var viewController: UIViewController?
    weak var floatingPanel: FloatingPanelController?

// MARK: - Transition

extension ModalFloatingPanelTransition: Transition {
    func open(_ viewController: UIViewController) {
        guard let selfController = self.viewController else { return }

            let delegate = viewController as? FloatingPanelControllerDelegate
        else {
            assertionFailure("`\(viewController.self)` must conform `FloatingPanelControllerDelegate`")

        let fpc = FloatingPanelController()
        floatingPanel = fpc
        delegate.floatingPanel = fpc
        fpc.set(contentViewController: viewController)
        // Other FloatingPanelController settings if needed

        let controller = selfController.presentedViewController ?? selfController
        controller.present(fpc, animated: animated) { [weak delegate, weak fpc] in
                let delegate = delegate,
                let fpc = fpc
            else {

    func close(_ viewController: UIViewController) {
        close(viewController, completion: {})

    func close(_: UIViewController, completion: @escaping () -> Void) {
        floatingPanel?.dismiss(animated: animated) { [weak self] in
            self?.floatingPanel?.set(contentViewController: nil)


View Github