Skip to content

Sedlacek-Solutions/SwiftUI-Navigation

Repository files navigation

SwiftUI-Navigation

Swift Package Manager GitHub stars GitHub forks GitHub contributors Pull Requests Badge Issues Badge

Description

SwiftUI-Navigation is a lightweight SwiftUI navigation library.

  • Leverages 1st-party APIs NavigationStack & NavigationDestination.
  • Never be confused about NavigationLink or NavigationPath again! (You don't need them)
  • Type-Safe Navigation (better performance than type-erasing).
  • Centralized Navigation Logic.
  • Dynamic Navigation Stack Management.
  • Unit Tested protocol implementations.
  • Zero 3rd party dependencies.

Table of Contents

  1. Requirements
  2. Installation
  3. Getting Started
  4. Passing Data Example
  5. View Extensions
  6. Under the hood

Requirements

Platform Minimum Version
iOS 16.0
macOS 13.0
tvOS 16.0
watchOS 9.0

Installation

You can install SwiftUI-Navigation using the Swift Package Manager.

  1. In Xcode, select File > Add Package Dependencies.

  1. Copy & paste the following into the Search or Enter Package URL search bar.
https://github.com/JamesSedlacek/SwiftUI-Navigation.git

  1. Xcode will fetch the repository & the SwiftUI-Navigation library will be added to your project.

Getting Started

  1. Create a Destination enum that conforms to the Destination protocol.
import Navigation
import SwiftUI

enum ExampleDestination: Destination {
    case detail
    case settings
    
    var body: some View {
        switch self {
        case .detail:
            DetailView()
        case .settings:
            SettingsView()
        }
    }
}
  1. Create a DestinationState object and wrap your RootView with a Navigator.
import SwiftUI
import Navigation

struct ContentView: View {
    @DestinationState private var destinations: [ExampleDestination] = []

    var body: some View {
        Navigator(path: $destinations) {
            Button("Go to Settings") {
                destinations.navigate(to: .settings)
            }
        }
    }
}
  1. Handle navigation using the DestinationState functions
/// Navigate back in the stack by a specified count.
func navigateBack(_ count: Int)

/// Navigate back to a specific destination in the stack.
func navigateBack(to destination: Destination)

/// Navigate to the root of the stack by emptying it.
func navigateToRoot()

/// Navigate to a specific destination by appending it to the stack.
func navigate(to destination: Destination)

/// Navigate to multiple destinations by appending them to the stack.
func navigate(to destinations: [Destination])

/// Replace the current stack with new destinations.
func replace(with destinations: [Destination])

Passing Data Example

import Navigation
import SwiftUI

enum ContentDestination: Destination {
    case detail(Color)
    case settings

    var body: some View {
        switch self {
        case .detail(let color):
            ColorDetail(color: color)
        case .settings:
            SettingsView()
        }
    }
}

struct ContentView: View {
    @DestinationState private var destinations: [ContentDestination] = []
    private let colors: [Color] = [.red, .green, .blue]

    var body: some View {
        Navigator(path: $destinations) {
            List(colors, id: \.self) { color in
                color
                    .onTapGesture {
                        destinations.navigate(to: .detail(color))
                    }
            }
        }
    }
}

struct ColorDetail: View {
    private let color: Color

    init(color: Color) {
        self.color = color
    }

    var body: some View {
        color.frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

View Extensions

SwiftUI-Navigation provides several View extensions to simplify common navigation and presentation patterns when working with Destination types.

navigationDestination(for: DestinationType.self)

This extension is a convenience wrapper around the standard SwiftUI navigationDestination(for:destination:) modifier. It's tailored for use with types conforming to Destination, automatically using the Destination instance itself as the destination view.

// Usage within a view:
// SomeView().navigationDestination(for: MyDestination.self)
// This is often handled automatically by Navigator.

Navigator uses this extension internally to set up navigation for your Destination enum.

sheet(item:onDismiss:)

Presents a sheet when a binding to an optional Destination & Identifiable item becomes non-nil. The content of the sheet is the Destination item itself.

  • item: A Binding to an optional Destination & Identifiable item.
  • onDismiss: An optional closure executed when the sheet dismisses.

Note: The Destination type used with this modifier must also conform to Identifiable.

import SwiftUI
import Navigation

// Ensure your Destination enum conforms to Identifiable.
// For enums with associated values, you might need to add an explicit `id`.
enum ModalDestination: Destination, Identifiable {
    case helpPage
    case userDetails(id: String)

    // Example of making it Identifiable
    var id: String {
        switch self {
        case .helpPage:
            return "helpPage"
        case .userDetails(let id):
            return "userDetails-\(id)"
        }
    }

    var body: some View {
        switch self {
        case .helpPage:
            HelpView() // Placeholder
        case .userDetails(let id):
            UserDetailsView(userID: id) // Placeholder
        }
    }
}

struct MyContentView: View {
    @State private var sheetItem: ModalDestination?

    var body: some View {
        Button("Show Help Sheet") {
            sheetItem = .helpPage
        }
        .sheet(item: $sheetItem)
    }
}

// Placeholder Views for example
struct HelpView: View { var body: some View { Text("Help Information") } }
struct UserDetailsView: View {
    let userID: String
    var body: some View { Text("Details for user \(userID)") }
}

navigationDestination(item:) (iOS 17.0+)

Available on iOS 17.0+, macOS 14.0+, tvOS 17.0+, watchOS 10.0+.

Presents a view using navigationDestination(item:destination:) when a binding to an optional Destination item becomes non-nil. The destination view is the Destination item itself. This is useful for modal-style presentations or alternative navigation flows that don't necessarily push onto the main NavigationStack.

  • item: A Binding to an optional Destination item.
import SwiftUI
import Navigation

// Assuming MyDetailDestination is a Destination enum
// enum MyDetailDestination: Destination { case info, settings ... }

@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
struct AnotherScreen: View {
    @State private var presentedDetail: MyDetailDestination? // MyDetailDestination conforms to Destination

    var body: some View {
        Button("Show Info Modally") {
            presentedDetail = .info // Assuming .info is a case in MyDetailDestination
        }
        .navigationDestination(item: $presentedDetail)
    }
}

Under the hood

The Navigator essentially wraps your view with a NavigationStack. It uses the navigationDestination(for: DestinationType.self) view extension (detailed in the "View Extensions" section) to automatically handle presenting the views associated with your Destination types.

// Simplified structure of Navigator's body:
NavigationStack(path: $path) { // $path is your @DestinationState's binding
    rootContent()
        .navigationDestination(for: DestinationType.self) // Uses the Destination-specific extension
}

About

SwiftUI library for abstracting navigation logic from views

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages