Skip to content

salahamassi/AppRouter-UIKit

Repository files navigation

AppRouter-UIKit

CI Swift iOS SPM

A lightweight, type-safe navigation framework for UIKit. Born from real production challenges at WINCH, AppRouter eliminates navigation boilerplate and enforces clean composition patterns.

The Problem

Navigation in UIKit apps often looks like this:

let vc = ServiceLocationViewController()
let presenter = ServiceLocationPresenter(output: WeakRef(vc))
let dataSource = AppDataSource()
let useCase = ServiceLocationUseCase(dataSource: dataSource, output: presenter, selectedLocationType: type)
vc.startLocationUpdate = useCase.startLocationUpdate
vc.confirmLocateServiceAction = useCase.confirmLocateServiceAction
navigationController?.pushViewController(vc, animated: true)

Composition logic scattered everywhere. Duplicate code. No consistency.

The Solution

router?.navigate(to: .serviceLocation(params: ["selectedLocationType": type]))

One line. All composition hidden behind a Route protocol.

Installation

// Package.swift
dependencies: [
    .package(url: "https://github.com/salahamassi/AppRouter-UIKit.git", from: "1.0.9")
]

Quick Start

1. Set up the router

// AppDelegate
var appRouter: AppRouter?

func application(_ application: UIApplication, didFinishLaunchingWithOptions...) -> Bool {
    let nav = UINavigationController()
    appRouter = AppRouter(window: window, rootViewController: nav)
    return true
}

2. Make view controllers routable

extension UIViewController: Routable {
    var router: AppRouter? { appRouter }
}

3. Create routes

class ProfileRoute: Route {
    var navigateType: NavigateType { .push }

    func create(_ router: AppRouter, _ params: [String: Any]?) -> UIViewController {
        let userId = params?["userId"] as? String ?? ""
        return ProfileViewController(userId: userId)
    }
}

4. Navigate

router?.navigate(to: ProfileRoute(), with: ["userId": "123"])

Navigation Types

Type Description
.push Push onto navigation stack
.present Modal presentation
.windowRoot Replace window root controller
.addChild(parent, view:, safeArea:) Add as child with nested router

Features

Type-Safe Route Composition

Each screen's dependencies are composed in its Route — not scattered across the codebase:

class OrderDetailsRoute: Route {
    var navigateType: NavigateType { .push }

    func create(_ router: AppRouter, _ params: [String: Any]?) -> UIViewController {
        guard let orderId = params?["orderId"] as? String else {
            fatalError("OrderDetailsRoute requires orderId")
        }
        let loader = OrderDetailsLoader()
        let presenter = OrderDetailsPresenter()
        return OrderDetailsViewController(orderId: orderId, loader: loader, presenter: presenter)
    }
}

Quick Routes with RouteFactory

For simple screens without complex composition:

let route: RouteFactory<SettingsViewController> = RouteFactory.createRoute(navigateType: .push)
router?.navigate(to: route)

Duplicate Prevention

Prevent the same screen from being pushed twice:

router?.canDuplicateViewControllers = false

Stack Management

router?.popViewController()                          // Pop one
router?.pop(numberOfScreens: 2)                      // Pop multiple
router?.remove(types: [LoadingViewController.self])   // Remove specific types
router?.removeAllAndKeep(types: [HomeViewController.self])  // Keep only these

Nested (Inner) Routers

For container patterns like tab bars with embedded navigation:

class OrdersRoute: Route {
    let parent: UIViewController
    let containerView: UIView

    var navigateType: NavigateType {
        .addChild(parent, view: containerView, safeArea: true)
    }

    func create(_ router: AppRouter, _ params: [String: Any]?) -> UIViewController {
        UINavigationController(rootViewController: OrdersViewController())
    }
}

// Child screens use innerRouter instead of the main router
innerRouter?.navigate(to: .orderDetails(params: ["orderId": order.id]))

Custom Transitions

class FadeRoute: Route {
    var navigateType: NavigateType { .push }
    var transition: CATransition? {
        let t = CATransition()
        t.type = .fade
        return t
    }

    func create(_ router: AppRouter, _ params: [String: Any]?) -> UIViewController {
        FadeViewController()
    }
}

Custom Presentation

class PhotoPreviewRoute: Route {
    private let animator = PhotosPresentDismissAnimator()

    var navigateType: NavigateType { .present }
    var modalPresentationStyle: UIModalPresentationStyle { .custom }
    var animatedTransitioningDelegate: UIViewControllerTransitioningDelegate? { animator }

    func create(_ router: AppRouter, _ params: [String: Any]?) -> UIViewController {
        PhotoPreviewViewController(medias: params?["medias"] as? [Media] ?? [])
    }
}

Organizing Routes

Use enums with Swift extensions for clean route access:

enum AuthRoutes {
    case signin
    case signup
    case pinCode(mobile: String)
}

extension AppRouter {
    func navigate(to route: AuthRoutes) {
        switch route {
        case .signin:
            navigate(to: SigninRoute())
        case .signup:
            navigate(to: SignupRoute())
        case .pinCode(let mobile):
            navigate(to: PinCodeRoute(), with: ["mobile": mobile])
        }
    }
}

// Usage
router?.navigate(to: .signin)
router?.navigate(to: .pinCode(mobile: "+966501234567"))

Testing

24 tests covering all navigation scenarios:

Executed 24 tests, with 0 failures in 7.6 seconds

Why AppRouter?

  • Zero dependencies — Pure UIKit, no third-party frameworks
  • Production-tested — Powers WINCH's logistics platform serving Gulf clients
  • Lightweight — Simple protocol-based API, easy to adopt incrementally
  • Testable — Router can be mocked for unit testing navigation logic

Author

Salah Nahed — Senior Mobile Engineer

License

MIT

About

A Swift library for managing navigation and screen composition in UIKit iOS applications

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages