LinkNavigator

repository·main·Indexed 19 days ago

https://github.com/forxifless/linknavigator

A SwiftUI navigation library that uses URL-like path strings to manage page stacks. It supports deep-linking, parameter injection, and complex navigation logic, designed for compatibility with Unidirectional Architectures such as MVI or The Composable Architecture (TCA). The library provides a comprehensive API for pushing, popping, replacing stacks, and managing modals, sheets, and alerts via RouteBuilder components.

Tokens
2.6K
Snippets
5
Records
6
Agent score
16%

What's inside LinkNavigator

  1. What is LinkNavigator?

    main
    LinkNavigator is a SwiftUI library that enables intuitive page navigation using URL path-like expressions. It supports deep-link style navigation, parameter injection during transitions, and is designed to work well with Uni-directional Architectures like MVI or The Composable Architecture (TCA).
  2. How RouteBuilder works

    main

    A RouteBuilder is the bridge between a path string and a SwiftUI View. It requires two main properties:

    1. matchPath: A unique string that identifies the route.
    2. build: A closure that receives the LinkNavigatorType, a dictionary of items (parameters), and the DependencyType. It must return a MatchingViewController (usually a WrappingController) containing the SwiftUI view.
    struct HomeRouteBuilder: RouteBuilder {
      var matchPath: String { "home" }
    
      var build: (LinkNavigatorType, [String: String], DependencyType) -> MatchingViewController? {
        { navigator, items, dependency in
          return WrappingController(matchPath: matchPath) { 
            HomePage(navigator: navigator) 
          }
        }
      }
    }
  3. Install LinkNavigator via Swift Package Manager

    main

    You can install LinkNavigator using Xcode's Package Manager or by adding it to your Package.swift file.

    Via Xcode UI:

    1. Go to the File menu -> Add Packages....
    2. Enter https://github.com/interactord/LinkNavigator.git in the Package URL field.

    Via Package.swift:

    let package = Package(
      name: "MyPackage",
      products: [
        .library(
          name: "MyPackage",
          targets: ["MyPackage"]),
      ],
      dependencies: [
        .package(url: "https://github.com/interactord/LinkNavigator.git", .upToNextMajor(from: "0.6.1"))
      ],
      targets: [
        .target(
          name: "MyPackage",
          dependencies: ["LinkNavigator"])
      ]
    )
  4. Setup Guide: Implementing LinkNavigator in SwiftUI

    main

    To integrate LinkNavigator, follow these three steps:

    Step 1: Define Infrastructure

    Implement four core components:

    1. AppDependency: A type conforming to DependencyType to manage external dependencies.
    2. AppRouterGroup: A type that returns an array of RouteBuilder objects.
    3. AppDelegate: Manages the LinkNavigator instance, injecting dependencies and builders.
    4. AppMain: The @main entry point that calls navigator.launch(paths:items:) to start the app.

    Step 2: Inject Navigator into Views

    Add a navigator property (of type LinkNavigatorType) to your page structs so it can be used for navigation within the view or its ViewModel.

    Step 3: Implement RouteBuilders

    For every page, create a struct conforming to RouteBuilder. Define its matchPath and a build closure that returns a MatchingViewController (typically using WrappingController).

    // 1. AppDependency
    struct AppDependency: DependencyType { }
    
    // 2. AppRouterGroup
    struct AppRouterGroup {
      var routers: [RouteBuilder] {
        [HomeRouteBuilder(), Page1RouteBuilder()] 
      }
    }
    
    // 3. AppDelegate
    final class AppDelegate: NSObject {
      var navigator: LinkNavigator {
        LinkNavigator(dependency: AppDependency(), builders: AppRouterGroup().routers)
      }
    }
    
    // 4. AppMain
    @main
    struct AppMain: App {
      @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
      var navigator: LinkNavigator { appDelegate.navigator }
    
      var body: some Scene {
        WindowGroup {
          navigator
            .launch(paths: ["home"], items: [:])
        }
      }
    }
    
    // Step 2: View Injection
    struct HomePage: View {
      let navigator: LinkNavigatorType
      var body: some View { ... }
    }
    
    // Step 3: RouteBuilder
    struct HomeRouteBuilder: RouteBuilder {
      var matchPath: String { "home" }
      var build: (LinkNavigatorType, [String: String], DependencyType) -> MatchingViewController? {
        { navigator, items, dependency in
          return WrappingController(matchPath: matchPath) { HomePage(navigator: navigator) }
        }
      }
    }
  5. Basic Navigation API Reference

    main

    Use the navigator instance to perform standard navigation tasks:

    • Push pages: next(paths:items:isAnimated:) pushes one or many pages.
    • Pop pages: remove(paths:) removes specific pages from the stack.
    • Go back: back(isAnimated:) returns to the prior page or dismisses a modal.
    • Smart navigation: backOrNext(path:items:isAnimated:) goes to a target page if it exists in the stack; otherwise, it pushes it.
    • Replace stack: replace(paths:items:isAnimated:) replaces the entire current stack with new pages.
    • Modals:
      • sheet(paths:items:isAnimated:) opens a page as a sheet.
      • fullSheet(paths:items:isAnimated:prefersLargeTitles:) opens a page as a full-screen cover.
    • Dismissal: close(isAnimated:completion:) closes the current modal.
    • Alerts: alert(target:model:) shows a system alert.
    // Push
    navigator.next(paths: ["page1", "page2"], items: [:], isAnimated: true)
    
    // Pop
    navigator.remove(paths: ["pageToRemove"])
    
    // Back
    navigator.back(isAnimated: true)
    
    // Smart navigation
    navigator.backOrNext(path: "targetPage", items: [:], isAnimated: true)
    
    // Replace stack
    navigator.replace(paths: ["main", "depth1", "depth2"], items: [:], isAnimated: true)
    
    // Modals
    navigator.sheet(paths: ["sheetPage"], items: [:], isAnimated: true)
    navigator.fullSheet(paths: ["page1", "page2"], items: [:], isAnimated: true, prefersLargeTitles: false)
    
    // Close modal
    navigator.close(isAnimated: true) { print("modal dismissed!") }
    
    // Alert
    let alertModel = Alert(
        title: "Title",
        message: "message",
        buttons: [.init(title: "OK", style: .default, action: { print("OK tapped") })],
        flagType: .default)
    navigator.alert(target: .default, model: alertModel)
  6. Advanced Navigation Techniques

    main

    LinkNavigator provides advanced control over the navigation stack and modal presentation:

    • Path Manipulation: Use navigator.range(path:) to get a sub-section of the current stack and combine it with new paths to perform complex replacements.
    • Root Navigation:
      • rootNext(paths:items:isAnimated:): Controls pages behind the current modal.
      • rootBackOrNext(path:items:isAnimated:): Navigates the root stack while a modal is active.
    • Custom Modal Styles: customSheet(paths:items:isAnimated:iPhonePresentationStyle:iPadPresentationStyle:prefersLargeTitles:) allows setting different presentation styles for iPhone and iPad.
    • Reloading Root: rootReloadLast(items:isAnimated:) forces a reload of the last page behind the modal (useful for triggering onAppear).
    // Edit complicated paths
    // current stack == ["home", "depth1", "depth2", "depth3"]
    // target stack == ["home", "depth1", "newDepth"]
    var new = navigator.range(path: "depth1") + ["newDepth"]
    navigator.replace(paths: new, items: [:], isAnimated: true)
    
    // Control pages behind modal
    navigator.rootNext(paths: ["targetPage"], items: [:], isAnimated: true)
    navigator.rootBackOrNext(path: "targetPage", items: [:], isAnimated: true)
    
    // Custom presentation styles
    navigator.customSheet(
        paths: ["sheetPage"],
        items: [:],
        isAnimated: true,
        iPhonePresentationStyle: .fullScreen,
        iPadPresentationStyle: .pageSheet,
        prefersLargeTitles: .none)
    
    // Reload last page behind modal
    navigator.rootReloadLast(items: [:], isAnimated: false)