AppRouter Documentation

repository·main·Indexed 19 days ago

https://github.com/dimillian/approuter

A generic, reusable navigation router for SwiftUI applications. It provides SimpleRouter for single-stack navigation and Router for complex tab-based navigation with independent stacks. The library includes built-in sheet management, support for URL deep linking via the DestinationType protocol, and tools for managing navigation paths and tab selections.

Tokens
5.3K
Snippets
8
Records
15
Agent score
17%

What's inside AppRouter

  1. How contextual routing works with URLs

    main

    Contextual routing allows the same URL path component to resolve to different destinations based on the preceding path segment. This mirrors web routing patterns (e.g., /users/detail vs /posts/detail).

    In your DestinationType.from(path:fullPath:parameters:) implementation, you can inspect the previousComponent (the element in fullPath immediately before the current path) to decide which enum case to return. This enables natural, REST-like URL structures.

    // Example URL patterns and their resulting destinations:
    // "myapp://users/detail?id=123"  → .userDetail(id: "123")
    // "myapp://posts/detail?id=456"  → .postDetail(id: "456") 
    // "myapp://detail?id=789"        → .detail(id: "789")
  2. Install AppRouter via Swift Package Manager

    main

    Add the AppRouter package to your project's dependencies using Swift Package Manager to enable generic navigation routing in SwiftUI.

    dependencies: [
        .package(url: "https://github.com/dimillian/AppRouter.git", from: "1.0.0")
    ]
  3. Use SimpleRouter for single-stack navigation

    main

    SimpleRouter is designed for applications using a single NavigationStack. It manages a single navigation path and a single sheet presentation.

    Implementation Steps:

    1. Define Types: Create enums for your destinations and sheets conforming to DestinationType and SheetType respectively.
    2. Initialize Router: Create a @State instance of SimpleRouter<Destination, Sheet>.
    3. Configure SwiftUI Views:
      • Bind NavigationStack(path:) to router.path.
      • Bind .sheet(item:) to router.presentedSheet.
      • Inject the router into the environment using .environment(router).
    4. Navigate: Use @Environment(SimpleRouter<Destination, Sheet>.self) to access the router in child views and call .navigateTo(_:) or .presentSheet(_:).

    Example

    import SwiftUI
    import AppRouter
    
    enum Destination: DestinationType {
        case detail(id: String)
        case settings
    }
    
    enum Sheet: SheetType {
        case compose
        var id: Int { hashValue }
    }
    
    struct ContentView: View {
        @State private var router = SimpleRouter<Destination, Sheet>()
        
        var body: some View {
            NavigationStack(path: $router.path) {
                HomeView()
                    .navigationDestination(for: Destination.self) { destination in
                        // Handle destination
                    }
            }
            .sheet(item: $router.presentedSheet) { sheet in
                // Handle sheet
            }
            .environment(router)
        }
    }
    
    struct HomeView: View {
        @Environment(SimpleRouter<Destination, Sheet>.self) private var router
        
        var body: some View {
            Button("Go to Detail") {
                router.navigateTo(.detail(id: "123"))
            }
        }
    }
    import SwiftUI
    import AppRouter
    
    // 1. Define your destination and sheet types
    enum Destination: DestinationType {
        case detail(id: String)
        case settings
        case profile(userId: String)
    }
    
    enum Sheet: SheetType {
        case compose
        case settings
        
        var id: Int { hashValue }
    }
    
    // 2. Use SimpleRouter
    struct ContentView: View {
        @State private var router = SimpleRouter<Destination, Sheet>()
        
        var body: some View {
            NavigationStack(path: $router.path) {
                HomeView()
                    .navigationDestination(for: Destination.self) { destination in
                        destinationView(for: destination)
                    }
            }
            .sheet(item: $router.presentedSheet) { sheet in
                sheetView(for: sheet)
            }
            .environment(router)
        }
        
        @ViewBuilder
        private func destinationView(for destination: Destination) -> some View {
            switch destination {
            case .detail(let id):
                Text("Detail \(id)")
            case .settings:
                Text("Settings")
            case .profile(let userId):
                Text("Profile \(userId)")
            }
        }
        
        @ViewBuilder
        private func sheetView(for sheet: Sheet) -> some View {
            switch sheet {
            case .compose:
                Text("Compose")
            case .settings:
                Text("Settings Sheet")
            }
        }
    }
    
    // 3. Navigate from anywhere in your app
    struct HomeView: View {
        @Environment(SimpleRouter<Destination, Sheet>.self) private var router
        
        var body: some View {
            VStack {
                Button("Go to Detail") {
                    router.navigateTo(.detail(id: "123"))
                }
                
                Button("Show Compose Sheet") {
                    router.presentSheet(.compose)
                }
            }
        }
    }
  4. Set up URL Deep Linking

    main

    AppRouter supports URL-based deep linking for both Router and SimpleRouter. To implement deep linking, follow these three steps:

    1. Implement URL Parsing: Your destination type must conform to DestinationType and implement the static func from(path:fullPath:parameters:) method. This method allows you to map URL paths and query parameters to specific enum cases. You can use the fullPath array to implement contextual routing (e.g., distinguishing between /users/detail and /posts/detail).
    2. Handle Incoming URLs: In your SwiftUI view hierarchy, use the .onOpenURL modifier to pass incoming URLs to your router instance using router.navigate(to: url).
    3. Configure URL Scheme: Register your app's custom URL scheme in your project's Info.plist under CFBundleURLTypes.
    enum Destination: DestinationType {
        case detail(id: String)
        case list
        case profile(userId: String)
        case userDetail(id: String)
        case postDetail(id: String)
        
        static func from(path: String, fullPath: [String], parameters: [String: String]) -> Destination? {
            guard let currentIndex = fullPath.firstIndex(of: path) else { return nil }
            let previousComponent = currentIndex > 0 ? fullPath[currentIndex - 1] : nil
            
            switch (previousComponent, path) {
            case ("users", "detail"):
                return .userDetail(id: parameters["id"] ?? "unknown")
            case ("posts", "detail"):
                return .postDetail(id: parameters["id"] ?? "unknown")
            case (_, "detail"):
                return .detail(id: parameters["id"] ?? "default")
            case (_, "list"):
                return .list
            case (_, "profile"):
                return .profile(userId: parameters["userId"] ?? "unknown")
            default:
                return nil
            }
        }
    }
  5. Use Router for tab-based navigation

    main

    Router is designed for applications using a TabView where each tab maintains its own independent navigation stack.

    Implementation Steps:

    1. Define Tab Type: Create an enum conforming to TabType. It must be CaseIterable and Identifiable.
    2. Define Destination and Sheet Types: Create enums conforming to DestinationType and SheetType.
    3. Initialize Router: Create a @State instance of Router<AppTab, Destination, Sheet>(initialTab: .home).
    4. Configure SwiftUI Views:
      • Bind TabView(selection:) to router.selectedTab.
      • For each tab, create a NavigationStack(path:) bound to router[tab] (the specific path for that tab).
      • Bind .sheet(item:) to router.presentedSheet.
    5. Navigate: Use the router to navigate within a specific tab using .navigateTo(_:for:).

    Example

    import SwiftUI
    import AppRouter
    
    enum AppTab: String, TabType, CaseIterable {
        case home, profile, settings
        var id: String { rawValue }
        var icon: String { "house" }
    }
    
    enum Destination: DestinationType {
        case detail(id: String)
        case list
    }
    
    enum Sheet: SheetType {
        case settings
        var id: Int { hashValue }
    }
    
    struct ContentView: View {
        @State private var router = Router<AppTab, Destination, Sheet>(initialTab: .home)
        
        var body: some View {
            TabView(selection: $router.selectedTab) {
                ForEach(AppTab.allCases) {
                    NavigationStack(path: $router[tab]) {
                        HomeView()
                            .navigationDestination(for: Destination.self) { dest in
                                // Handle destination
                            }
                    }
                    .tabItem { Label(tab.rawValue, systemImage: tab.icon) }
                    .tag(tab)
                }
            }
            .sheet(item: $router.presentedSheet) { sheet in
                // Handle sheet
            }
        }
    }
    import SwiftUI
    import AppRouter
    
    enum AppTab: String, TabType, CaseIterable {
        case home, profile, settings
        
        var id: String { rawValue }
        
        var icon: String {
            switch self {
            case .home: return "house"
            case .profile: return "person"  
            case .settings: return "gear"
            }
        }
    }
    
    enum Destination: DestinationType {
        case detail(id: String)
        case list
        case profile(userId: String)
    }
    
    enum Sheet: SheetType {
        case settings
        case profile
        case compose
        
        var id: Int { hashValue }
    }
    
    struct ContentView: View {
        @State private var router = Router<AppTab, Destination, Sheet>(initialTab: .home)
        
        var body: some View {
            TabView(selection: $router.selectedTab) {
                ForEach(AppTab.allCases) { tab in
                    NavigationStack(path: $router[tab]) {
                        HomeView()
                            .navigationDestination(for: Destination.self) { destination in
                                destinationView(for: destination)
                            }
                    }
                    .tabItem {
                        Label(tab.rawValue.capitalized, systemImage: tab.icon)
                    }
                    .tag(tab)
                }
            }
            .sheet(item: $router.presentedSheet) { sheet in
                sheetView(for: sheet)
            }
        }
        
        @ViewBuilder
        private func destinationView(for destination: Destination) -> some View {
            switch destination {
            case .detail(let id):
                Text("Detail \(id)")
            case .list:
                Text("List")
            case .profile(let userId):
                Text("Profile \(userId)")
            }
        }
        
        @ViewBuilder  
        private func sheetView(for sheet: Sheet) -> some View {
            switch sheet {
            case .settings:
                Text("Settings")
            case .profile:
                Text("Profile Sheet")
            case .compose:
                Text("Compose")
            }
        }
    }
  6. Test deep links in iOS Simulator

    main

    To test your deep link implementation without a physical device, use the xcrun simctl command in your terminal:

    # Open deep link in simulator
    xcrun simctl openurl booted "myapp://detail?id=123"
    
    # Test contextual routing
    xcrun simctl openurl booted "myapp://users/detail?id=user123"
    xcrun simctl openurl booted "myapp://posts/detail?id=post456"
  7. Use type aliases for cleaner Router syntax

    main

    Because Router and SimpleRouter use multiple generic parameters (e.g., Router<Tab, Destination, Sheet>), the syntax can become verbose. It is a best practice to define type aliases at the top level of your app to simplify @Environment and @State declarations.

    // Define once in your app
    typealias AppRouter = Router<AppTab, Destination, Sheet>
    typealias AppSimpleRouter = SimpleRouter<Destination, Sheet>
    
    // Then use the cleaner syntax everywhere
    @Environment(AppRouter.self) private var router
    @State private var router = AppRouter(initialTab: .home)
  8. Create and share deep link URLs

    main

    You can programmatically generate deep link URLs using the URL.deepLink helper extension. This is useful for features like sharing content via UIActivityViewController.

    Format: scheme://destination1/destination2?param1=value1

    // Using the URL helper extension
    let url = URL.deepLink(
        scheme: "myapp",
        destinations: [Destination.detail(id: "123")],
        parameters: ["source": "share"]
    )
    
    // Share the URL
    if let url = url {
        let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil)
        // Present activity controller
    }
  9. Navigate to a URL programmatically

    main

    You can trigger navigation using a URL string or a URL object directly through the router instance. This works for both SimpleRouter and Router (which handles tab-based navigation).

    struct HomeView: View {
        @Environment(SimpleRouter<Destination, Sheet>.self) private var router
        
        var body: some View {
            VStack {
                Button("Deep Link to Detail") {
                    router.navigate(to: "myapp://detail?id=456")
                }
                
                Button("Navigate with URL") {
                    let url = URL(string: "myapp://list/detail?id=789")!
                    router.navigate(to: url)
                }
            }
        }
    }
  10. SimpleRouter API Reference

    main

    SimpleRouter is a @MainActor observable class for managing a single navigation stack and sheet presentation.

    Properties

    • path: [Destination]: The current navigation path.
    • presentedSheet: Sheet?: The currently presented sheet.

    Methods

    • navigateTo(_:): Navigate to a new destination.
    • popNavigation(): Pop the last destination from the stack.
    • popToRoot(): Clear the entire navigation stack.
    • presentSheet(_:): Present a sheet.
    • dismissSheet(): Dismiss the current sheet.
    • navigate(to:): Navigate using a URL or URL string.
  11. Router API Reference

    main

    Router is a @MainActor observable class for managing tab-based navigation where each tab has its own independent stack.

    Properties

    • selectedTab: Tab: The currently selected tab.
    • presentedSheet: Sheet?: The currently presented sheet (global across tabs).
    • selectedTabPath: [Destination]: The navigation path for the currently active tab.

    Methods

    • navigateTo(_:for:): Navigate to a destination within a specific tab.
    • popNavigation(for:): Pop the last destination from a specific tab's stack.
    • popToRoot(for:): Clear the navigation stack for a specific tab.
    • presentSheet(_:): Present a sheet.
    • dismissSheet(): Dismiss the current sheet.
    • navigate(to:): Navigate using a URL or URL string.