Nivelir

repository·main·Indexed 43 days ago

https://github.com/hhru/nivelir

A Domain Specific Language (DSL) for navigation in iOS and tvOS applications. Nivelir provides a simplified, chainable, and compile-time safe syntax for managing view controller transitions using core abstractions like Screen, ScreenNavigator, and ScreenRoute.

Tokens
3.7K
Snippets
13
Records
14
Agent score
53%

What's inside Nivelir

  1. Use Screen factories with Dependency Injection

    main

    In multi-module projects, you can pass Screen factories through DI by erasing their types. This prevents module leakage while maintaining navigation capabilities.

    • Screen/eraseToAnyScreen(): Erases the type but preserves the container type (e.g., AnyTabsScreen or AnyStackScreen), allowing for tab switching or stack changes.
    • Screen/eraseToAnyModalScreen(): Erases the container type to UIViewController, ensuring the container type does not extend beyond the module boundary.

    You can also share ScreenRoute objects (like ScreenWindowRoute) between modules to define complex navigation paths that can be executed by a central navigator.

    // Erasing a factory for DI
    struct Screens {
        func homeScreen() -> AnyTabsScreen {
            HomeScreen(services: services, screens: self).eraseToAnyScreen()
        }
    }
    
    // Executing a shared route from DI
    let screens: Screens
    let navigator = ScreenNavigator()
    navigator.navigate(to: screens.showRoomListRoute())
  2. Core Nivelir Concepts

    main

    Nivelir's architecture is built around several key abstractions:

    • Screen: A protocol used to define a piece of content. It encapsulates the logic of how to build a UIViewController from a set of parameters.
    • ScreenNavigator: The central engine that executes navigation commands. It manages the lifecycle of navigation requests.
    • ScreenRoute: A chainable DSL used within the navigate block to describe a sequence of navigation actions (e.g., pushing, popping, or presenting screens).
    • ScreenAction: Represents individual navigation operations or side effects (like showing an alert, sharing content, or opening a URL) that can be executed by the navigator.
  3. Quick Start: Implement navigation with Screen and ScreenNavigator

    main

    Nivelir uses a Screen protocol to define view controllers and a ScreenNavigator to execute navigation commands.

    1. Define a Screen: Implement the Screen protocol. The build(navigator:) method is responsible for returning the UIViewController instance.
    2. Execute Navigation: Use a ScreenNavigator instance and its navigate method. Inside the closure, use the provided route to chain commands like .top(), .popToRoot(), .push(), and .present().
    // 1. Define your ViewController
    class SomeViewController: UIViewController {
        let color: UIColor
        init(color: UIColor) { 
            self.color = color
            super.init(nibName: nil, bundle: nil) 
        }
        required init?(coder: NSCoder) { fatalError() }
        override func viewDidLoad() { super.viewDidLoad(); view.backgroundColor = color }
    }
    
    // 2. Implement the Screen protocol
    struct SomeScreen: Screen {
        let color: UIColor
        func build(navigator: ScreenNavigator) -> UIViewController {
            SomeViewController(color: color)
        }
    }
    
    // 3. Navigate
    let navigator = ScreenNavigator()
    
    navigator.navigate { route in
        route
            .top(.stack)
            .popToRoot()
            .push(SomeScreen(color: .red))
            .push(SomeScreen(color: .green)) { route in
                route.present(SomeScreen(color: .blue))
            }
    }
  4. Dismiss a modal and push to the underlying stack

    main

    To close a modally presented screen and immediately push a new screen onto the navigation stack that was underneath it, use a chain involving dismiss(), top(.stack), and push().

    Note: ScreenThenable/dismiss(animated:) only closes the presented screen, unlike the standard UIViewController.dismiss which might dismiss a whole hierarchy.

    // container is the modally presented view controller
    func showChat(withID chatID: String) {
        navigator.navigate(from: container?.presenting) { route in
            route
                .dismiss() // 1. Close the modal
                .top(.stack) // 2. Find the top UINavigationController below
                .push(screens.chatScreen(chatID: chatID)) // 3. Push new screen
        }
    }
  5. Install Nivelir via CocoaPods

    main

    Add Nivelir to your Podfile specifying the target and the version constraint. Then, run pod install to integrate the framework into your workspace.

    # In your Podfile
    platform :ios, '14.0'
    use_frameworks!
    
    target '<Your Target Name>' do
        pod 'Nivelir', '~> 1.9.15'
    end
    $ pod install
  6. Quick Start with Nivelir

    main

    Nivelir uses a DSL to define screens and navigate between them. To use it, follow these three steps:

    1. Define your View Controller: Create a standard UIViewController (or UIView based controller) that handles the UI logic.
    2. Implement the Screen protocol: Create a struct that conforms to Screen. This struct acts as a builder that takes parameters and returns the actual UIViewController via the build(navigator:) method.
    3. Use ScreenNavigator to navigate: Initialize a ScreenNavigator and use the navigate method to chain navigation actions like push, popToRoot, and present.
    // 1. The View Controller
    class SomeViewController: UIViewController {
        let color: UIColor
        init(color: UIColor) { 
            self.color = color
            super.init(nibName: nil, bundle: nil) 
        }
        required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = color
        }
    }
    
    // 2. The Screen implementation
    struct SomeScreen: Screen {
        let color: UIColor
        func build(navigator: ScreenNavigator) -> UIViewController {
            SomeViewController(color: color)
        }
    }
    
    // 3. Navigation
    let navigator = ScreenNavigator()
    navigator.navigate { route in
        route
            .top(.stack)
            .popToRoot()
            .push(SomeScreen(color: .red))
            .push(SomeScreen(color: .green)) { route in
                route.present(SomeScreen(color: .blue))
            }
    }
  7. Navigate with custom animations

    main

    Nivelir supports custom animations for several navigation types:

    • Root replacement: Use ScreenRootCustomAnimation via route.setRoot(to:animation:).
    • Stack changes: Use ScreenStackCustomAnimation via route.replace(with:animation:) or route.push(_:animation:).
    • Tab switching: Use ScreenTabCustomAnimation.
    • Modal presentation: Use route.present(_:) combined with Screen/withModalTransitioningDelegate(_:) to pass a UIViewControllerTransitioningDelegate.

    Prebuilt animations like .crossDissolve can be passed to the animation parameter.

    // Replace root with cross-dissolve
    navigator.navigate { route in
        route.setRoot(to: screens.homeScreen(), animation: .crossDissolve)
    }
    
    // Custom modal transition
    navigator.navigate(from: container) { route in
        route.present(
            screens
                .floatingActionSheetScreen()
                .withModalTransitioningDelegate(CardModalTransitionAnimator())
        )
    }
  8. Update screen data after navigation

    main

    After navigating to a screen, you can update its data using two primary methods:

    1. Direct Access: Use get(body:) to access the container (the view controller type returned by your Screen factory) and call its methods directly.
    2. Refresh Protocol: Use refresh() if the container implements the ScreenRefreshableContainer protocol.
    // Using get(body:) to call a specific method on the view controller
    navigator.navigate { route in
        route
            .last(.container(of: chatScreen))
            .makeVisible()
            .get { chatViewController in
                chatViewController.update(newMessage: "Hello 👋")
            }
    }
    
    // Using refresh() for ScreenRefreshableContainer
    navigator.navigate { route in
        route
            .last(.container(of: chatScreen))
            .makeVisible()
            .refresh()
    }
    navigator.navigate { route in
        route
            .last(.container(of: chatScreen))
            .makeVisible()
            .get { chatViewController in
                chatViewController.update(newMessage: "Hello 👋")
            }
    }
  9. Search for and show existing screens

    main

    You can find specific screens within the current navigation hierarchy using ScreenThenable/first(_:) or ScreenThenable/last(_:) with a predicate. To make the found screen visible, chain the makeVisible(stackAnimation:tabsAnimation:dissmissAnimated:) method.

    Warning: makeVisible() will close any currently presented modal screens. If the modal contains unsaved user input, perform manual navigation instead to avoid data loss.

    let navigator = ScreenNavigator()
    let chatScreen = ChatScreen(chatID: 2)
    
    navigator.navigate { route in
        route
            .last(.container(of: chatScreen))
            .makeVisible()
    }
  10. Install Nivelir via Swift Package Manager

    main

    To integrate Nivelir into your Xcode project using Swift Package Manager, add the following dependency to your Package.swift file. Ensure you specify "Nivelir" as a dependency for the specific Target where you intend to use the framework.

    // swift-tools-version:5.7
    import PackageDescription
    
    let package = Package(
        name: "MyPackage",
        products: [
            .library(name: "MyPackage", targets: ["MyPackage"])
        ],
        dependencies: [
            .package(url: "https://github.com/hhru/Nivelir.git", from: "1.9.15")
        ],
        targets: [
            .target(name: "MyPackage", dependencies: ["Nivelir"])
        ]
    )
  11. Install Nivelir via Carthage

    main

    To use Carthage, first ensure it is installed via Homebrew. Then, add Nivelir to your Cartfile and run the update command to build the framework. Finally, drag the generated Nivelir.framework into your Xcode project.

    # Install Carthage
    $ brew update
    $ brew install carthage
    
    # Add to Cartfile
    github "hhru/Nivelir" ~> 1.9.15
    
    # Build
    $ carthage update