Contents

prittejani/appcoordinator

A lightweight, type-safe navigation coordinator for SwiftUI.

✨ Features

  • βœ… Type-safe routing via generic Route enums
  • βœ… NavigationStack + NavigationPath based β€” no UIKit dependency
  • βœ… Push, pop, popToRoot out of the box
  • βœ… Sheet and full-screen cover support
  • βœ… Alert coordination
  • βœ… Deep link handling (URL β†’ Route)
  • βœ… Works in small and large modular apps
  • βœ… Clean separation β€” views know nothing about navigation logic

πŸ“‹ Requirements

| Platform | Minimum | |----------|---------| | iOS | 16.0+ | | macOS | 13.0+ | | Swift | 5.9+ | | Xcode | 15.0+ |


πŸ“¦ Installation

Swift Package Manager (Xcode)

  1. Open your project in Xcode
  2. Go to File β†’ Add Package Dependencies
  3. Paste the URL:
https://github.com/prittejani/AppCoordinator
  1. Select Up to Next Major Version β†’ 1.0.0
  2. Click Add Package

Swift Package Manager (Package.swift)

dependencies: [
    .package(url: "https://github.com/prittejani/AppCoordinator", from: "1.0.0")
],
targets: [
    .target(
        name: "YourApp",
        dependencies: ["AppCoordinator"]
    )
]

πŸš€ Quick Start

1. Define your routes

import AppCoordinator

enum HomeRoute: Route {
    case dashboard
    case profile(userID: String)
    case settings
}

2. Create a coordinator

import AppCoordinator
import SwiftUI

@MainActor
final class HomeCoordinator: Coordinator {

    let router = Router<HomeRoute>()

    func start() { }

    @ViewBuilder
    func view(for route: HomeRoute) -> some View {
        switch route {
        case .dashboard:
            DashboardView()
        case .profile(let userID):
            ProfileView(userID: userID)
        case .settings:
            SettingsView()
        }
    }
}

3. Wire it into your app

import SwiftUI
import AppCoordinator

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            CoordinatorView(coordinator: HomeCoordinator()) { coordinator in
                DashboardView()
            }
        }
    }
}

4. Navigate from any view

struct DashboardView: View {
    @EnvironmentObject var router: Router<HomeRoute>

    var body: some View {
        VStack(spacing: 16) {
            Button("Open Profile") {
                router.push(.profile(userID: "abc123"))
            }
            Button("Open Settings as Sheet") {
                router.present(sheet: .settings)
            }
            Button("Go Back") {
                router.pop()
            }
            Button("Go to Root") {
                router.popToRoot()
            }
        }
    }
}

πŸ”— Deep Linking

let handler = DeepLinkHandler<HomeRoute> { url in
    guard url.host == "home" else { return nil }
    switch url.path {
    case "/dashboard": return .dashboard
    case "/settings":  return .settings
    default:           return nil
    }
}

// In your App
.onOpenURL { url in
    if let route = handler.handle(url: url) {
        router.push(route)
    }
}

🚨 Alert Coordination

// In your coordinator or view
let alertCoordinator = AlertCoordinator()

alertCoordinator.show(AlertItem(
    title: "Delete Item",
    message: "This action cannot be undone.",
    primaryButton: .destructive(Text("Delete")) {
        // handle delete
    },
    secondaryButton: .cancel()
))

// Attach to your view
ContentView()
    .withAlertCoordinator(alertCoordinator)

πŸ“„ Sheet Coordination

enum MySheet: Identifiable, Hashable {
    case camera
    case imagePicker
    case paywall

    var id: Self { self }
}

let sheetCoordinator = SheetCoordinator<MySheet>()

// Present a sheet
sheetCoordinator.present(.camera)

// Present full screen
sheetCoordinator.present(.paywall, fullScreen: true)

// Dismiss
sheetCoordinator.dismiss()

// Attach to your view
ContentView()
    .withSheetCoordinator(sheetCoordinator) { sheet in
        switch sheet {
        case .camera:      CameraView()
        case .imagePicker: ImagePickerView()
        case .paywall:     PaywallView()
        }
    }

πŸ— Architecture Overview

App Entry (@main)
    └── AppCoordinator (root)
            β”œβ”€β”€ AuthCoordinator
            β”‚     β”œβ”€β”€ Router<AuthRoute>
            β”‚     └── Auth Views (Login, Register, ForgotPassword)
            β”œβ”€β”€ HomeCoordinator
            β”‚     β”œβ”€β”€ Router<HomeRoute>
            β”‚     └── Home Views (Dashboard, Profile)
            └── SettingsCoordinator
                  β”œβ”€β”€ Router<SettingsRoute>
                  └── Settings Views (Preferences, Account, Help)

πŸ“ Package Structure

Sources/
└── AppCoordinator/
    β”œβ”€β”€ AppCoordinator.swift        ← public umbrella
    β”œβ”€β”€ Core/
    β”‚   β”œβ”€β”€ Coordinator.swift       ← base protocol
    β”‚   β”œβ”€β”€ Router.swift            ← NavigationPath wrapper
    β”‚   └── CoordinatorView.swift   ← root SwiftUI view
    β”œβ”€β”€ Sheet/
    β”‚   └── SheetCoordinator.swift  ← sheet & fullScreenCover
    β”œβ”€β”€ Alert/
    β”‚   └── AlertCoordinator.swift  ← alert management
    └── DeepLink/
        └── DeepLinkHandler.swift   ← URL β†’ Route mapping

πŸ§ͺ Testing

Because Router is a plain ObservableObject with no SwiftUI dependency, you can unit test navigation logic directly:

import XCTest
@testable import AppCoordinator

final class HomeCoordinatorTests: XCTestCase {

    @MainActor
    func testPushRoute() {
        let coordinator = HomeCoordinator()
        coordinator.router.push(.profile(userID: "123"))
        XCTAssertEqual(coordinator.router.path.count, 1)
    }

    @MainActor
    func testPopToRoot() {
        let coordinator = HomeCoordinator()
        coordinator.router.push(.dashboard)
        coordinator.router.push(.settings)
        coordinator.router.popToRoot()
        XCTAssertEqual(coordinator.router.path.count, 0)
    }
}

πŸ“ License

AppCoordinator is available under the MIT License. See the LICENSE file for full details.


🀝 Contributing

Contributions are welcome! Please open an issue first to discuss what you'd like to change.

  1. Fork the repo
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

<p align="center">Made with ❀️ for the SwiftUI community</p>

Package Metadata

Repository: prittejani/appcoordinator

Default branch: main

README: README.md