Skip to content

ahmedelmoughazy/NavigationKit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

42 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

NavigationKit

A lightweight navigation system for SwiftUI applications that provides programmatic navigation with hierarchical state management.

Swift Platform SPM

Demo

Demo

Features

  • Type-Safe Navigation: Strongly-typed routing with compile-time safety
  • Layered Navigation: Present views on top of each other, modals within modals, all managed automatically
  • Modal Presentations: Built-in support for sheets and full-screen covers
  • Programmatic Control: Push, pop, present, and dismiss from anywhere
  • Debug Support: Comprehensive hierarchy logging for development
  • Reactive Updates: Combine publishers for navigation state changes
  • Animation Control: Optional animation for all navigation actions

Requirements

  • iOS 16.0+
  • Swift 5.9+
  • Xcode 15.0+

Installation

Swift Package Manager

Add NavigationKit to your project using Swift Package Manager:

dependencies: [
    .package(url: "https://github.com/ahmedelmoughazy/NavigationKit.git", from: "0.2.0")
]

Then add it to your target dependencies:

.target(
    name: "YourTarget",
    dependencies: ["NavigationKit"]
)

Quick Start

1. Create Your Views

Mark your views with the @Routable macro:

import SwiftUI
import NavigationKit

@Routable
struct ProfileView: View {
    let userId: String
    
    var body: some View {
        Text("Profile for user: \(userId)")
    }
}

@Routable
struct SettingsView: View {
    var body: some View {
        Text("Settings")
    }
}

2. Set Up Navigation

Create a router and wrap your root view with BaseNavigation:

import SwiftUI
import NavigationKit

@main
struct MyApp: App {
    private let router = Router()
    
    var body: some Scene {
        WindowGroup {
            BaseNavigation(router: router) {
                RootView()
            }
        }
    }
}

3. Navigate

Access the router from any view using @EnvironmentObject and navigate programmatically:

struct RootView: View {
    @EnvironmentObject var router: Router
    
    var body: some View {
        VStack {
            Button("Go to Profile") {
                router.push(destination: ProfileView(userId: "123"))
            }
            
            Button("Open Settings as Sheet") {
                router.present(destination: SettingsView(), as: .sheet)
            }
        }
    }
}

Usage Guide

Router API

Stack Navigation

// Push a new destination
router.push(destination: ProfileView(userId: "123"))

// Pop the current view
router.pop()

// Pop to a specific destination
router.pop(to: HomeView())

// Pop to presentation (clear current stack)
router.popToPresentation()

// Pop all and dismiss all modals
router.popAll()

Modal Presentation

// Present as sheet
router.present(destination: SettingsView(), as: .sheet)

// Present as full-screen cover
router.present(destination: DetailView(), as: .fullScreenCover)

// Dismiss current modal
router.dismiss()

Advanced Navigation

// Insert destination at specific index
router.insert(destination: HomeView(), at: 0)

// Remove specific destinations
router.remove(destinations: ProfileView(userId: "123"))

// Replace entire navigation path
router.applyPath([HomeView(), ProfileView(userId: "456")])

Animation Control

All navigation methods support optional animation control:

router.push(destination: .profileView(userId: "123"), animated: false)
router.pop(animated: false)
router.present(destination: .settingsView, as: .sheet, animated: false)

Route Tracking

Monitor navigation state changes reactively:

// Get current route
let currentRoute: [String] = router.currentRoute

// Subscribe to route changes
router.currentRoutePublisher
    .sink { route in
        print("Navigation changed: \(route)")
    }
    .store(in: &cancellables)

Alert System

NavigationKit includes a built-in alert system that works seamlessly with modal presentations.

Why a Custom Alert System?

When using standard SwiftUI alert modifiers alongside sheet or full-screen cover presentations, showing an alert can cause the modal presentation to be dismissed unexpectedly. This is a known SwiftUI behavior where alert presentation can interfere with modal lifecycle management.

NavigationKit's alert system solves this problem by:

  • Modal-Safe Presentation: Alerts won't dismiss sheets or full-screen covers
  • State Management: Properly integrated with the navigation hierarchy
  • Alert Replacement: Seamlessly replace one alert with another
  • Automatic Cleanup: Properly synchronizes state with the router

Usage

Present alerts through the router's alertItem property:

struct MyView: View {
    @EnvironmentObject var router: Router<Route>
    
    var body: some View {
        Button("Show Alert") {
            router.presentAlert(alertItem: AlertItem(
                    title: "Confirmation",
                    message: "Are you sure?",
                    actionButtons: [
                        AlertActionButton(title: "Confirm", style: .primary) {
                            // Handle confirmation
                        },
                        AlertActionButton(title: "Cancel", style: .cancel)
                    ]
                )
            )
        }
    }
}

Alert Button Styles

NavigationKit provides four button styles to match your alert's intent:

  • .primary: The main action (emphasized with keyboard shortcut)
  • .secondary: Alternative actions (default style)
  • .destructive: Dangerous actions like delete
  • .cancel: Dismisses without action

Dismissing Alerts

Alerts are automatically dismissed when:

  • The user taps any action button.
  • You call router.dismissAlert().

Advanced Features

Debug Logging

NavigationKit provides powerful logging capabilities to help debug navigation flows.

Configuring Logging

Set the logging style when creating the router:

// Disable logging (default)
let router = Router(loggingStyle: .disabled)

// Enable hierarchical logging (tree view)
let router = Router(loggingStyle: .hierarchical)

// Enable flat logging (array view)
let router = Router(loggingStyle: .flat)

When logging is enabled, the navigation hierarchy is automatically printed whenever navigation state changes.

Logging Styles

Disabled (default) - No logging output

Hierarchical - Tree view with indentation:

🎯 Router#a1b2c
  πŸ“± Path: [home, profile]
  πŸ“„ Sheet: settings
  └── 🎯 Router#d3e4f
      πŸ“± Path: [details]

Flat - Array view with sequential listing:

Routers: [
  🎯 Router#a1b2c | πŸ“± Path: [home, profile] | πŸ“„ Sheet: settings
  🎯 Router#d3e4f | πŸ“± Path: [details]
]

Dynamic Configuration

You can change the logging style at any time:

// Switch to hierarchical logging
router.loggingStyle = .hierarchical

// Disable logging
router.loggingStyle = .disabled

Manual Logging

You can also manually trigger logging at any time:

router.debugPrintCompleteHierarchy()

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

NavigationKit is available under the MIT license. See the LICENSE file for more info.

Author

Ahmed Elmoughazy - @ahmedelmoughazy

Acknowledgments

Built with Swift Macros and SwiftSyntax for powerful compile-time code generation.

About

A SwiftUI navigation package for iOS 16+

Topics

Resources

License

Stars

Watchers

Forks

Languages