Skip to content

A lightweight SwiftUI Navigation Coordinator package that supports stack navigation and modal presentation.

License

Notifications You must be signed in to change notification settings

silkodenis/swiftui-navigation-coordinator

Repository files navigation

License swift

SwiftUI Navigation Coordinator

Screenshot 1 Screenshot 2 Screenshot 3 Screenshot 4

About the Project

This project provides a lightweight Navigation Coordinator, using SwiftUI NavigationStack (available from iOS 16).

Core Features

The current implementation covers 6 main transitions:

Stack Navigation:
  • push — navigates forward to a new view.
  • pop — returns to the previous view.
  • unwind — performs a multi-level return.
  • popToRoot — returns to the root view.
Modal Presentation:
  • present — displays a modal view, overlaying it on top of current content.
  • dismiss — closes the current modal view and returns to the underlying content.

Requirements

  • iOS: iOS 16.0+
  • macOS: macOS 13.0+
  • watchOS: watchOS 9.0+
  • tvOS: tvOS 16.0+

Add with Swift Package Manager:

  1. Open Xcode and select “File” > “Add Packages…”
  2. Enter the URL of the package repository.
  3. Follow the instructions to complete the installation.

Getting Started

1. Configure the NavigableScreen Enum

Start by creating an enum Screen to represent the different screens in your app. Ensure it conforms to the NavigableScreen protocol:

import NavigationCoordinator

enum Screen {
    case login
    case movies
    case settings
    // etc.
}

extension Screen: NavigableScreen {
    @ViewBuilder
    var view: some View {
        switch self {
        case .login: LoginView()
        case .movies: MoviesView()
        case .settings: SettingsView()
        }
    }
}
2. Define Typealiases

Define typealias to simplify the usage of the types used with your coordinator:

import NavigationCoordinator

typealias SegueModifier = RegisterSegueModifier<Screen>
typealias Coordinator = NavigationCoordinator<Screen>
typealias RootView = NavigationStackRootView<Screen>
3. Configure the App Entry Point

Set up the app entry point using the RootView to define the initial screen:

import SwiftUI

@main
struct MainApp: App {
    var body: some Scene {
        WindowGroup {
            RootView(.login)
        }
    }
}

Usage Examples

Push
import SwiftUI

struct LoginView: View {
    @EnvironmentObject var coordinator: Coordinator
    
    var body: some View {
        Button("Movies") {
            coordinator.push(.movies)
        }
    }
}
Pop
import SwiftUI

struct MoviesView: View {
    @EnvironmentObject var coordinator: Coordinator
    
    var body: some View {
        Button("back") {
            coordinator.pop()
        }
    }
}
PopToRoot
import SwiftUI

struct SettingsView: View {
    @EnvironmentObject var coordinator: Coordinator
    
    var body: some View {
        Button("login") {
            coordinator.popToRoot()
        }
    }
}
Unwind Use a unique identifier for your unwind segues. If a segue becomes no longer relevant, it will be automatically removed from the coordinator. Using `onUnwind()` modifier is completely safe, tested, and does not involve any memory leaks or unintended calls.
import SwiftUI

// B View
// 🟦🟦🅰🟦🟦🟦🟦🟦🟦🅱️  
struct B: View {
    @EnvironmentObject var coordinator: Coordinator
    
    var body: some View {
        Button("pop to A") {
            coordinator.unwind(to: "identifier" /*, with: Any?*/)
        }
    }
}

// A View
// 🟦🟦🅰️
struct A: View {
    var body: some View {
        VStack {}
            .onUnwind(segue: "identifier") /*{ Any? in }*/
    }
}

onUnwind() will always be called before onAppear().

Present
import SwiftUI

/*
               [B]
[ ][ ][ ][ ][ ][A]
*/
struct A: View {
    @EnvironmentObject var coordinator: Coordinator
    
    var body: some View {
        Button("present") {
            coordinator.present(.B)
        }
    }
}
Dismiss
import SwiftUI

/*
               [B][ ][ ][ ][CL]
[ ][ ][ ][ ][ ][A]
*/
struct CL: View {
    @EnvironmentObject var coordinator: Coordinator
    
    var body: some View {
        Button("dismiss") {
            coordinator.dismiss(/*to: "identifier" /*, with: Any?*/*/)
        }
    }
}

/*
[ ][ ][ ][ ][ ][A]
*/
struct A: View {
    @EnvironmentObject var coordinator: Coordinator
    
    var body: some View {
        VStack {}
            // Not necessary. Only if you need to capture an onDismiss event.
            .onDismiss(segue: "identifier") /*{ Any? in }*/
    }
}

Project examples

Reporting Issues

I welcome any issues you find within the project. If you encounter bugs or have suggestions for improvements, please feel free to create an issue on the GitHub repository.

License

Apache License 2.0. See the LICENSE file for details.