Skip to content

Type-safe, protocol-driven navigation framework for SwiftUI. Supports push navigation, sheets, full-screen covers, and tab-based routing with independent stacks. Built with Swift 6.0 concurrency and @observable for modern iOS/macOS apps.

License

Notifications You must be signed in to change notification settings

br3akzero/BigTime

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BigTime

Type-safe, protocol-driven navigation framework for SwiftUI apps

BigTime is a reusable Swift Package that provides a robust navigation system for SwiftUI applications. It supports push navigation, sheet presentations, full-screen covers, and tab-based navigation with independent stacks per tab.

Features

  • âś… Type-safe routing using protocol-based enums
  • âś… Push navigation with NavigationStack
  • âś… Sheet presentations with customizable detents and drag indicators
  • âś… Hierarchical sheets - sheets can present child sheets with parent-child tracking
  • âś… Full-screen covers for immersive experiences
  • âś… Tab-based navigation with isolated stacks per tab
  • âś… Universal overlay with Router-level and TabRouter-level support
  • âś… Screen view tracking with optional callbacks
  • âś… Dismiss handlers for post-navigation actions
  • âś… Built-in logging using OSLog
  • âś… Swift 6.0 with full concurrency support

Installation

Swift Package Manager

Add BigTime to your project via Xcode:

  1. File → Add Package Dependencies...
  2. Enter the repository URL
  3. Select the version/branch
  4. Add to your target

Or add it to your Package.swift:

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

Quick Start

1. Define Your Route Enum

Create a Route enum that conforms to Routable:

import BigTime
import SwiftUI

enum Route: Routable {
    case home
    case profile
    case settings
    case detail(id: String)
}

// MARK: - Hashable
extension Route: Hashable {
    static func == (lhs: Route, rhs: Route) -> Bool {
        switch (lhs, rhs) {
        case (.home, .home), (.profile, .profile), (.settings, .settings):
            return true
        case (.detail(let lID), .detail(let rID)):
            return lID == rID
        default:
            return false
        }
    }

    func hash(into hasher: inout Hasher) {
        switch self {
        case .home:
            hasher.combine("home")
        case .profile:
            hasher.combine("profile")
        case .settings:
            hasher.combine("settings")
        case .detail(let id):
            hasher.combine("detail")
            hasher.combine(id)
        }
    }
}

// MARK: - Identifiable
extension Route: Identifiable {
    var id: UUID { UUID() }
}

// MARK: - CustomStringConvertible
extension Route: CustomStringConvertible {
    var description: String {
        switch self {
        case .home: return "Home"
        case .profile: return "Profile"
        case .settings: return "Settings"
        case .detail(let id): return "Detail(\(id))"
        }
    }
}

// MARK: - View
extension Route: View {
    var body: some View {
        switch self {
        case .home:
            HomeScreen()
        case .profile:
            ProfileScreen()
        case .settings:
            SettingsScreen()
        case .detail(let id):
            DetailScreen(id: id)
        }
    }
}

2. Use RouterView in Your App

import BigTime
import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            RouterView<Route>(root: .home)
        }
    }
}

3. Navigate in Your Views

import BigTime
import SwiftUI

struct HomeScreen: View {
    @Environment(Router<Route>.self) private var router

    var body: some View {
        VStack {
            Button("Go to Profile") {
                router.push(.profile)
            }

            Button("Show Settings Sheet") {
                router.sheet(.settings)
            }

            Button("Show Detail Full Screen") {
                router.fullScreenCover(.detail(id: "123"))
            }
        }
    }
}

Tab-Based Navigation

1. Define Your TabRoute Enum

import BigTime
import SwiftUI

enum TabRoute: TabRoutable {
    case home
    case search
    case profile

    var rootRoute: Route {
        switch self {
        case .home: return .home
        case .search: return .search
        case .profile: return .profile
        }
    }

    var title: String {
        switch self {
        case .home: return "Home"
        case .search: return "Search"
        case .profile: return "Profile"
        }
    }

    var icon: String {
        switch self {
        case .home: return "house.fill"
        case .search: return "magnifyingglass"
        case .profile: return "person.circle"
        }
    }
}

// MARK: - Hashable, Identifiable, CaseIterable
extension TabRoute: Hashable, Identifiable, CaseIterable {
    var id: String { title }

    static var allCases: [TabRoute] {
        [.home, .search, .profile]
    }
}

// MARK: - CustomStringConvertible
extension TabRoute: CustomStringConvertible {
    var description: String { title }
}

2. Use TabRouterView in Your App

import BigTime
import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            TabRouterView<TabRoute>()
        }
    }
}

3. Navigate Between Tabs

struct HomeScreen: View {
    @Environment(TabRouter<TabRoute>.self) private var tabRouter
    @Environment(Router<Route>.self) private var router

    var body: some View {
        VStack {
            Button("Switch to Search Tab") {
                tabRouter.switchTab(to: .search)
            }

            Button("Push Detail in Current Tab") {
                router.push(.detail(id: "abc"))
            }
        }
    }
}

Advanced Features

Screen View Tracking

Track screen views for analytics:

RouterView(root: .home) { screenName in
    // Log to your analytics service
    Analytics.track(screen: screenName)
}

Dismiss Handlers

Execute code after modal dismissal:

router.sheet(.settings) {
    // Refresh data after settings are dismissed
    Task { await loadUserData() }
}

Custom Sheet Detents

Control sheet presentation sizes:

router.sheet(
    .settings,
    detents: [.medium, .large],
    dragIndicator: .visible
)

Custom Logging Subsystem

Provide a custom subsystem for logging:

let router = Router(
    root: .home,
    subsystem: "com.myapp.navigation"
)

Hierarchical Sheet Presentation

Present sheets from within sheets with automatic parent-child tracking. Simply use sheet() everywhere - the framework automatically detects if you're already in a sheet and creates a hierarchical presentation:

struct SettingsSheet: View {
    @Environment(Router<Route>.self) private var router

    var body: some View {
        VStack {
            Button("Show Privacy Settings") {
                // Just use sheet() - it automatically becomes a child sheet
                router.sheet(.privacySettings)
            }

            Button("Show Appearance Settings") {
                router.sheet(
                    .appearanceSettings,
                    detents: [.medium],
                    dragIndicator: .visible
                )
            }
        }
    }
}

// Present the parent sheet
router.sheet(.settings)

// From within the settings sheet, present another sheet
// It automatically becomes a child sheet
router.sheet(.privacySettings)

// Dismiss the child sheet (returns to parent)
router.dismissSheet()

// Dismiss all sheets in the hierarchy at once
router.dismissAllSheets()

Key Points:

  • Use sheet() everywhere - it automatically detects hierarchical presentation
  • If called when no sheet is present, it creates a new root sheet
  • If called from within an existing sheet, it creates a child sheet
  • Each child maintains its own detents, drag indicators, and dismiss handlers
  • dismissSheet() dismisses the topmost sheet and returns to its parent
  • dismissAllSheets() dismisses the entire sheet hierarchy
  • All dismiss handlers are called in reverse order (child to parent)

Universal Overlay

Present persistent views that float above navigation content but below modals. Common use cases include mini-players, floating action buttons, or persistent banners.

Two overlay scopes:

  1. Router-level overlay - For standalone RouterView usage. Tied to a specific router.
  2. TabRouter-level overlay - For TabRouterView usage. Persists across tab switches.
// Router-level overlay (dismissed when switching tabs)
router.universalOverlay(.miniPlayer(station))
router.dismissUniversalOverlay()

// TabRouter-level overlay (persists across tabs)
tabRouter.universalOverlay(.miniPlayer(station))
tabRouter.dismissUniversalOverlay()

Mutual exclusion: Only one overlay can be active at a time. Presenting a TabRouter overlay automatically dismisses any Router overlay, and vice versa.

Layer order (bottom to top):

  1. NavigationStack / TabView (base content)
  2. Universal Overlay
  3. Sheets
  4. Full Screen Cover

API Reference

Protocols

Routable

Protocol that your Route enum must conform to:

  • Hashable - For NavigationStack path
  • Identifiable - For SwiftUI list/forEach
  • CustomStringConvertible - For logging
  • View - To render the route
  • @MainActor - Must be used on the main actor only

TabRoutable

Protocol that your TabRoute enum must conform to:

  • Hashable - For tab selection
  • Identifiable - For SwiftUI tabs
  • CustomStringConvertible - For logging
  • CaseIterable - To enumerate all tabs
  • associatedtype RouteType: Routable - The route type for this tab
  • @MainActor - Must be used on the main actor only

Classes

Router<Route: Routable>

Observable router managing navigation state:

Properties:

  • routes: [Route] - Navigation stack
  • rootRoute: Route - Base route
  • sheetStack: [SheetPresentation<Route>] - Stack of presented sheets (supports hierarchy)
  • sheetRoute: Route? - Current sheet (computed from sheetStack)
  • fullScreenCoverRoute: Route? - Current cover

Methods:

  • push(_ route: Route) - Push onto stack
  • pop() - Pop from stack
  • popToRoot() - Clear stack
  • switchRoot(_ root: Route) - Change root route
  • sheet(_ route: Route, detents:dragIndicator:onDismiss:) - Present sheet (auto-detects hierarchical presentation)
  • fullScreenCover(_ route: Route, onDismiss:) - Present cover
  • dismissSheet() - Dismiss topmost sheet (returns to parent if hierarchy exists)
  • dismissAllSheets() - Dismiss all sheets in the hierarchy
  • dismissFullScreenCover() - Dismiss cover

SheetPresentation<Route: Routable>

Represents a single sheet in the presentation hierarchy:

Properties:

  • route: Route - The route being presented
  • detents: Set<PresentationDetent>? - Presentation detents
  • dragIndicator: Visibility? - Drag indicator visibility
  • onDismiss: (() -> Void)? - Dismiss callback

TabRouter<TabRoute: TabRoutable>

Observable router managing tab navigation:

Properties:

  • selectedTab: TabRoute - Current tab
  • routers: [TabRoute: Router<TabRoute.RouteType>] - Per-tab routers
  • currentRouter: Router<TabRoute.RouteType> - Router for selected tab
  • universalOverlayRoute: TabRoute.RouteType? - Current overlay (persists across tabs)
  • hasUniversalOverlay: Bool - Check if overlay is presented

Methods:

  • router(for tab: TabRoute) - Get router for specific tab
  • switchTab(to tab: TabRoute) - Switch to tab
  • universalOverlay(_:animation:) - Present persistent overlay
  • dismissUniversalOverlay(animation:) - Dismiss overlay

Views

RouterView<Route: Routable>

SwiftUI view managing navigation:

Initializers:

  • init(router: Router<Route>, onScreenView:) - Use existing router
  • init(root: Route, subsystem:onScreenView:) - Create new router

TabRouterView<TabRoute: TabRoutable>

SwiftUI view managing tab navigation:

Initializers:

  • init(tabRouter: TabRouter<TabRoute>, onScreenView:) - Use existing tab router
  • init(selectedTab:subsystem:onScreenView:) - Create new tab router

Requirements

  • iOS 18.0+ / macOS 15.0+ / watchOS 11.0+ / tvOS 18.0+ / visionOS 2.0+
  • Swift 6.0+
  • Xcode 16.0+

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a 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

Support

Author

Created by @br3akzero

Acknowledgments

Built with using Swift 6.0 and SwiftUI

About

Type-safe, protocol-driven navigation framework for SwiftUI. Supports push navigation, sheets, full-screen covers, and tab-based routing with independent stacks. Built with Swift 6.0 concurrency and @observable for modern iOS/macOS apps.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages