prittejani/appcoordinator
A lightweight, type-safe navigation coordinator for SwiftUI.
β¨ Features
- β
Type-safe routing via generic
Routeenums - β
NavigationStack+NavigationPathbased β 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)
- Open your project in Xcode
- Go to File β Add Package Dependencies
- Paste the URL:
https://github.com/prittejani/AppCoordinator- Select Up to Next Major Version β
1.0.0 - 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.
- Fork the repo
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - 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