🧭 SwiftUI navigation done right

Overview

🧭 NavigationKit

swift v5.3 platform iOS deployment target iOS 14 YouTube tutorial

NavigationKit is a lightweight library which makes SwiftUI navigation super easy to use.

💻 Installation

📦 Swift Package Manager

Using Swift Package Manager, add it as a Swift Package in Xcode 11.0 or later, select File > Swift Packages > Add Package Dependency... and add the repository URL:

https://github.com/rebeloper/NavigationKit.git

Manual Installation

Download and include the NavigationKit folder and files in your codebase.

📲 Requirements

  • iOS 14+
  • Swift 5.3+

🎬 Video Tutorial

SwiftUI Navigation - How to Navigate in SwiftUI Apps on YouTube

This tutorial was made for v.0.1.0. I have improved and made NavigationKit even easier to use since this video. Read on to see how to use the newest version.

👉 Import

Import NavigationKit into your View

import NavigationKit

🧳 Features

Here's the list of the awesome features NavigationKit has:

  • default / custom transitions
  • push
  • push to view with id
  • pop
  • pop to view with id
  • pop to root
  • present (as modal / full screen)
  • may disable swipe down to dismiss on modal
  • dismiss
  • dismiss to root
  • combine push with present (good for showing Login / Onboarding / Tutorial)
  • built in Navigation Bars as view-modifiers (or build and use your own dream nav bar)
  • works perfectly with TabView

In SwiftUI navigtion is handeled by the NavigationView and NavigationLink. At the moment these views have some limitations:

  • transition animations cannot be turned off or customised;
  • we can't navigate back either to root (i.e. the first app view), or to a specific view;
  • we can't push programmatically without using a View;
  • customizing the NavigationBar is limited or it has to be done via UINavigationBar.appearance() (using UIKit 😒 );
  • presenting a view modally is done with the .sheet and .fullScreenCover view-modifiers adding confusion to the NavigationLink's push like pattern;

NavigationKitView is a view that mimics all the behaviours belonging to the standard NavigationView, but it adds the features listed here above. You have to wrap your view hierarchy inside a NavigationKitView:

import NavigationKit

struct RootView: View {
    var body: some View {
        NavigationKitView {
            Tab_0_0_View()
        }
    }
}

You can even customise transitions and animations in some different ways. The NavigationKitView will apply them to the hierarchy:

  • you could decide to go for no transition at all by creating the navigation stack this way NavigationStackView(transitionType: .none);
  • you could create the navigation stack with a custom transition:
import NavigationKit

struct RootView: View {
    var body: some View {
        NavigationKitView(transitionType: .custom(.scale)) {
            Tab_0_0_View()
        }
    }
}

NavigationKitView has a default easing for transitions. The easing can be customised during the initialisation

struct RootView: View {
    var body: some View {
        NavigationKitView(transitionType: .custom(.scale), easing: .spring(response: 0.5, dampingFraction: 0.25, blendDuration: 0.5)) {
            Tab_0_0_View()
        }
    }
}

Important: The above is the recommended way to customise the easing function for your transitions. Please, note that you could even specify the easing this other way:

NavigationKitView(transitionType: .custom(AnyTransition.scale.animation(.spring(response: 0.5, dampingFraction: 0.25, blendDuration: 0.5))))

Attaching the easing directly to the transition? Don't do this. SwiftUI has still some problems with implicit animations attached to transitions, so it may not work. For example, implicit animations attached to a .slide transition won't work.

⬅️ Push

In order to navigate forward you have to push with an optional delay:

import NavigationKit

struct Tab_0_0_View: View {
    
    @EnvironmentObject private var navigation: Navigation
    
    var body: some View {
        VStack {
            Button {
                navigation.push(Tab_0_1_View(), delay: 1.5)
            } label: {
                Text("Next")
            }
            Spacer()
        }
    }
}

Make sure you are using a view model in order for values to persist between push/pop operations. SwiftUI resets all the properties of a view marked with @State every time the view is removed from a view hierarchy. For the NavigationKitView this is a problem because when I come back to a previous view (with a pop operation) I want all my view controls to be as I left them before (for example I want my TextFields to contain the text I previously typed in). It seems that the solution to this problem is using the .id modifier specifying an id for the views I don't want SwiftUI to reset. According to the Apple documentation the .id modifier:

Generates a uniquely identified view that can be inserted or removed.

but again, it seems that this API is currently not working as expected (take a look at this interesting post: https://swiftui-lab.com/swiftui-id/). In order to workaround this problem, then, you have to use @ObservableObject when you need to make some state persist between push/pop operations.

import NavigationKit

struct Tab_0_0_View: View {
    
    @EnvironmentObject private var navigation: Navigation
    @ObservedObject private var viewModel = Tab_0_0_ViewModel()
    
    var body: some View {
        VStack {
            
            TextField("Type something...", text: $viewModel.text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            
            Button {
                self.viewModel.fetchData { (result) in
                    switch result {
                    case .success(let finished):
                        if finished {
                            navigation.push(Tab_0_2_View())
                        } else {
                            print("Something went wrong")
                        }
                    case .failure(let err):
                        print(err.localizedDescription)
                    }
                }
            } label: {
                Text("Push after model operation")
            }
            Spacer()
        }
    }
}

🆔 Specifying an ID

It's not mandatory, but if you want to come back to a specific view at some point later you need to specify an ID for that view:

Button {
    navigation.push(Tab_0_1_View(), withId: "Tab_0_1_View")
} label: {
    Text("Next")
}

You will be able to pop to this view using the id. Read on. 🤓

➡️ Pop

Pop operation works as the push operation, with an optional delay:

Button {
    navigation.pop(delay: 1.5)
} label: {
    Label("Back", systemImage: "chevron.backward")
}

which pops to the previous view. You can even specify a destination for your pop operation:

Button {
    navigation.pop(to: .view(withId: "Tab_0_1_View"))
} label: {
    Text("Pop to Tab_0_1_View")
}

We can also pop to root like so:

Button {
    navigation.pop(to: .root)
} label: {
    Text("Pop to Root")
}

🚧 NavigationBar

NavigationKit replaces NavigationView altogether. In order to see a navigation bar you can create your own or use the built in view modifiers. You must add them as a modifier of a VStack which contains a Spacer to push its content up.

Inline navigation bar

VStack {
    ...
    Spacer()
}
.inlineNavigationBar(titleView:
            Text("Tab_0_1_View").bold(),
        leadingView:
            Button {
                navigation.pop()
            } label: {
                Label("Back", systemImage: "chevron.backward")
            },
        trailingView:
            Button {
                navigation.push(Tab_0_2_View())
            } label: {
                Text("Next")
            },
        backgroundView:
            Color(.secondarySystemBackground).edgesIgnoringSafeArea(.top)
)

Large navigation bar

VStack {
    ...
    Spacer()
}
.largeNavigationBar(titleView:
            Text("Tab_0_0_View").bold().lineLimit(1),
        leadingView:
            EmptyView(),
        trailingView:
            Button {
                navigation.push(Tab_0_1_View(), withId: "Tab_0_1_View")
            } label: {
                Text("Next")
            },
        backgroundView:
            Color(.secondarySystemBackground).edgesIgnoringSafeArea(.top)
)

Custom navigation bar

var body: some View {
    VStack {
        ...
        Spacer()
    }.customNavigationBar(titleView:
                            HStack {
                                Text("TODAY").font(.title).fontWeight(.light)
                                Spacer()
                                Text(todayString().uppercased()).font(.title).fontWeight(.light)
                            },
                          backgroundView:
                            Color(.secondarySystemBackground).edgesIgnoringSafeArea(.top)
    )
}

func todayString() -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "EEE MM/dd"
    return formatter.string(from: Date())
}

⬆️ Present

Presenting a modal is a bit diferent than pushing:

  1. create a @State variable for your view;
  2. add a Sheet or FullScreenSheet view with an optional onDismiss callback. You must add it to the view hierarchy. Don't worry they are EmptyViews;
  3. activate the modal with present()

IMPORTANT NOTE: you can present a NavigationKitView inside a Sheet / FullScreenSheet 😎

import NavigationKit

struct Tab_1_0_View: View {
    
    // 1.
    @State private var navigationForTab_0_0_View = false
    @State private var navigationForTab_1_1_View = false
    
    @State private var navigationForTab_0_0_View_onDismiss = false
    @State private var navigationForTab_1_1_View_onDismiss = false
    
    var body: some View {
        VStack {
            Button {
                // 3.
                navigationForTab_0_0_View_onDismiss.present()
            } label: {
                Text("Present with onDismiss callback")
            }
            
            Button {
                // 3.
                navigationForTab_1_1_View_onDismiss.present()
            } label: {
                Text("Present with onDismiss callback")
            }
            
            Spacer()
            
            // 2.
            Sheet(isPresented: $navigationForTab_0_0_View) {
                NavigationKitView {
                    Tab_0_0_View() // <- contains push navigation
                }
            }
            
            // 2.
            FullScreenSheet(isPresented: $navigationForTab_1_1_View) {
                NavigationKitView {
                    Tab_1_1_View()
                }
            }
            
            // 2.
            Sheet(isPresented: $navigationForTab_0_0_View_onDismiss) {
                print("Dismissed Sheet. Do something here.")
            } content: {
                NavigationKitView {
                    Tab_0_0_View()
                }
            }
            
            // 2.
            FullScreenSheet(isPresented: $navigationForTab_1_1_View_onDismiss) {
                print("Dismissed FullScreenSheet. Do something here.")
            } content: {
                NavigationKitView {
                    Tab_1_1_View()
                }
            }
        }
        .padding()
        .largeNavigationBar(titleView:
                    Text("Tab_1_0_View").bold().lineLimit(1),
                leadingView:
                    Button {
                        // 3.
                        navigationForTab_0_0_View.present()
                    } label: {
                        Text("Present Navigation")
                    },
                trailingView:
                    Button {
                        // 3.
                        navigationForTab_1_1_View.present()
                    } label: {
                        Text("Present")
                    },
                backgroundView:
                    Color(.tertiarySystemBackground).edgesIgnoringSafeArea(.top)
        )
    }
}

⬇️ Dismiss

Here's how you can dismiss the modal:

  1. grab the presentationMode environment
  2. dimiss with it's wrappedValue
struct Tab_1_1_View: View {
    
    // 1.
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        VStack {
            Color(.systemRed).edgesIgnoringSafeArea(.all)
        }
        .largeNavigationBar(titleView:
                    Text("Tab_1_1_View").bold().lineLimit(1),
                leadingView:
                    Button {
                        // 2.
                        presentationMode.wrappedValue.dismiss()
                    } label: {
                        Text("Dismiss")
                    },
                trailingView:
                    EmptyView(),
                backgroundView:
                    Color(.tertiarySystemBackground).edgesIgnoringSafeArea(.top)
        )
    }
}

You may also disable swipe down on the Sheet:

Sheet(isPresented: $navigationForTab_1_3_View) {
    NavigationKitView {
        Tab_1_3_View().disableSwipeToDismiss()
    }
}

If you want to dismiss to root you want to use @Bindings and dismiss in order. 0.25 is the optimal delay:

struct Tab_1_3_View: View {
    @Environment(\.presentationMode) var presentationMode
    
    @Binding var rootView: Bool
    @Binding var secondRootView: Bool
    @Binding var thirdRootView: Bool
    
    var body: some View {
        VStack {
            Color(.systemRed).edgesIgnoringSafeArea(.all)
        }
        .largeNavigationBar(titleView:
                    Text("Tab_1_3_View").bold().lineLimit(1),
                leadingView:
                    EmptyView(),
                trailingView:
                    Button {
                        DispatchQueue.main.asyncAfter(deadline: .now()) {
                            thirdRootView.dismiss()
                        }
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
                            secondRootView.dismiss()
                        }
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                            rootView.dismiss()
                        }
                    } label: {
                        Text("Dismiss to Root")
                    },
                backgroundView:
                    Color(.tertiarySystemBackground).edgesIgnoringSafeArea(.top)
        )
    }
}

🪁 Demo project

For a comprehensive Demo project check out: NavigationKitDemo

✍️ Contact

rebeloper.com / YouTube / Shop / Mentoring

📃 License

The MIT License (MIT)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Comments
  • Is readme not up to date ?

    Is readme not up to date ?

    in your readme you said use

    NavigationKitView { }

    but when I use it it said isActive is required "Missing argument for parameter 'isActive' in call"

    and also it show error "Cannot find type 'Navigation' in scope" !!

    opened by X901 2
  • Use of multiple .pushesAsRoot in same View navigates to random Views

    Use of multiple .pushesAsRoot in same View navigates to random Views

    Hello @rebeloper,

    I am exploring NavigationKit and there is one problem happening in my project. Let me explain you the problem first:

    I have created one RootView as main View and added two Text with .pushesAsRoot property with which should navigate to two different Views but it is navigating wrong View if there are multiple .pushesAsRoot used in same View.

    Here is the video of the problem:

    https://user-images.githubusercontent.com/76034194/103065442-99c6db00-45dc-11eb-8b6b-9df56509f0f5.mp4

    Code snippets:

    @main
    struct NavigationDemoApp: App {
        var body: some Scene {
            WindowGroup {
                NavigationView{
                    // main view
                    RootView()
                }
                .rootable()
            }
        }
    }
    

    RootView with two .pushesAsRoot()

    struct RootView: View {
        
        var body: some View {
            VStack{
                Text("Root Controller")
                    .font(.title)
                    .padding(.bottom, 100)
                
                VStack(alignment: .center, spacing: 50){
                    // Push button 1
                    Button {
                    } label: {
                        Text("Push to Child 1")
                            .foregroundColor(.white)
                            .frame(width: 200, height: 50)
                            .pushesAsRoot(Child1())
                    }
                    .background(Color.blue)
                    .clipShape(Capsule())
                    .padding(.bottom)
                    
                    //Push button 2
                    Button {
                    } label: {
                        Text("Push to AfterLogin 1")
                            .foregroundColor(.white)
                            .frame(width: 200, height: 50)
                            .pushesAsRoot(AfterLogin1())
                    }
                    .background(Color.blue)
                    .clipShape(Capsule())
                }
            }
        }
    }
    

    Child1 View

    struct Child1: View {
        
        var body: some View {
            VStack{
                
                Text("Child 1")
                    .font(.title)
                    .padding(.bottom, 100)
                
                // Push button
                Button {
                } label: {
                    Text("Push to Child 2")
                        .foregroundColor(.white)
                        .frame(width: 200, height: 50)
                        .pushes(Child2())
                }
                .background(Color.blue)
                .clipShape(Capsule())
                .padding(.bottom)
                
                // Pop to root button
                Button {
                } label: {
                    Text("Pop to Root View")
                        .foregroundColor(.white)
                        .frame(width: 200, height: 50)
                        .dismissesToRoot()
                }
                .background(Color.red)
                .clipShape(Capsule())
            }
            .navigationBarHidden(true)
        }
    }
    

    AfterLogin1 View

    struct AfterLogin1: View {
        
        var body: some View {
            VStack{
                
                Text("AfterLogin 1")
                    .font(.title)
                    .padding(.bottom, 100)
                
                // Push button
                Button {
                } label: {
                    Text("Push to AfterLogin 2")
                        .foregroundColor(.white)
                        .frame(width: 200, height: 50)
                        .pushes(AfterLogin2())
                }
                .background(Color.blue)
                .clipShape(Capsule())
                .padding(.bottom)
                
                // Pop to root button
                Button {
                } label: {
                    Text("Pop to Root View")
                        .foregroundColor(.white)
                        .frame(width: 200, height: 50)
                        .dismissesToRoot()
                }
                .background(Color.red)
                .clipShape(Capsule())
            }
            .navigationBarHidden(true)
        }
    }
    

    Let me know the solution or guide me what's the problem in the code? Thanks!

    opened by saumilshah200191 1
  • Button is not clickable. Only Text is clickable.

    Button is not clickable. Only Text is clickable.

    Hello @rebeloper,

    This package is very helpful and thanks for that. I would like to get update on how it will work with Button and other components.

    Problem: When I was trying to use this with button then only Text is clickable and not the Button. Please see the attached video for the problem.

    https://user-images.githubusercontent.com/76034194/102853509-7538fa80-4446-11eb-8fbe-72f64ce62571.mov

    Code snippet of the button:

    Button {
    } label: {
       Text("Push to Child 2")
              .foregroundColor(.white)
              .frame(width: 200, height: 50)
              .pushes(Child2())
    }
    .background(Color.blue)
    .clipShape(Capsule())
    

    Please help me with the solution of this. Thanks!

    opened by saumilshah200191 1
  • Default UINavigationController animation?

    Default UINavigationController animation?

    Hi, quick question - how do I replicate the default NavigationView's transition animation?

    It's not a plain .move, it's both move and offset of the original one at a half distance. Any idea on how to implement this using NavigationKit?

    I've got:

    let pushTrans = AnyTransition.asymmetric(insertion: .move(edge: .trailing), removal: .offset(x: -UIScreen.main.bounds.size.width / 2, y: 0))
            let popTrans = AnyTransition.asymmetric(insertion: .move(edge: .leading), removal: .offset(x: UIScreen.main.bounds.size.width / 2, y: 0))
    

    But that doesn't change the z-value and looks incorrect.

    opened by kutakmir 1
  • Using NavigationKit along with other environmental objects and @Published variables

    Using NavigationKit along with other environmental objects and @Published variables

    Hi Alex,

    First of all, your NavigationKit looks super awesome, I was really happy when I found your framework.

    However in my app I'm using another environmental object with @Published variables. As soon as I access them e.g. via an .onAppear the push from the Navigationkit is not working anymore. Do you have any idea why that could be the case?

    opened by Broker89 4
Owner
Alex Nagy
🖥 Co-founder of rebeloper. I'm never more than a few inches away from something with an  logo. https://youtube.com/rebeloper
Alex Nagy
A drop-in universal library helps you to manage the navigation bar styles and makes transition animations smooth between different navigation bar styles

A drop-in universal library helps you to manage the navigation bar styles and makes transition animations smooth between different navigation bar styles while pushing or popping a view controller for all orientations. And you don't need to write any line of code for it, it all happens automatically.

Zhouqi Mo 3.3k Dec 21, 2022
sRouting - The lightweight navigation framework for SwiftUI.

sRouting The lightweight navigation framework for SwiftUI. Overview sRouting using the native navigation mechanism in SwiftUI. It's easy to handle nav

Shiro 8 Aug 15, 2022
FlowStacks allows you to hoist SwiftUI navigation and presentation state into a Coordinator

FlowStacks allow you to manage complex SwiftUI navigation and presentation flows with a single piece of state. This makes it easy to hoist that state into a high-level coordinator view. Using this pattern, you can write isolated views that have zero knowledge of their context within the navigation flow of an app.

John Patrick Morgan 471 Jan 3, 2023
Change SwiftUI Navigation Bar Color for different View

SwiftUINavigationBarColor Change SwiftUI NavigationBar background color per screen. Usage For NavigationBarColor to work, you have to set the Navigati

Hai Feng Kao 18 Jul 15, 2022
Navigation helpers for SwiftUI applications build with ComposableArchitecture

Swift Composable Presentation ?? Description Navigation helpers for SwiftUI applications build with ComposableArchitecture. More info about the concep

Dariusz Rybicki 52 Dec 14, 2022
Make SwiftUI Navigation be easy

VNavigator VNavigator is a clean and easy-to-use navigation in SwiftUI base on UINavigationController in UIKit Installation From CocoaPods CocoaPods i

Vu Vuong 10 Dec 6, 2022
Tools for making SwiftUI navigation simpler, more ergonomic and more precise.

SwiftUI Navigation Tools for making SwiftUI navigation simpler, more ergonomic and more precise. Motivation Tools Navigation overloads Navigation view

Point-Free 1.1k Jan 1, 2023
SwiftUINavigation provides UIKit-like navigation in SwiftUI

SwiftUINavigation About SwiftUINavigation provides UIKit-like navigation in Swif

Bhimsen Padalkar 1 Mar 28, 2022
A lightweight iOS mini framework that enables programmatic navigation with SwiftUI, by using UIKit under the hood.

RouteLinkKit A lightweight iOS mini framework that enables programmatic navigation with SwiftUI. RouteLinkKit is fully compatible with native Navigati

Αθανάσιος Κεφαλάς 4 Feb 8, 2022
SwiftUINavigator: a lightweight, flexible, and super easy library which makes SwiftUI navigation a trivial task

The logo is contributed with ❤️ by Mahmoud Hussein SwiftUINavigator is a lightwe

OpenBytes 22 Dec 21, 2022
Simple iOS app to showcase navigation with coordinators in SwiftUI + MVVM.

SimpleNavigation Simple iOS app to showcase the use of the Coordinator pattern using SwiftUI and MVVM. The implementation is as easy as calling a push

Erik Lopez 7 Dec 6, 2022
Custom navigation swiftui NavigationLink NavigationView

Custom navigation swiftui Experimenting with navigation link. if you find this idea interesting you can take and expend it into a more powerful soluti

Igor 5 Dec 2, 2022
Backported SwiftUI navigation APIs introduced in WWDC22

Navigation Backport This package uses the navigation APIs available in older SwiftUI versions (such as NavigationView and NavigationLink) to recreate

John Patrick Morgan 532 Dec 29, 2022
Models UI navigation patterns using TCA

Composable Navigation The Composable Navigation is a Swift Package that builds on top of The Composable Architecture (TCA, for short). It models UI na

Michael Heinzl 41 Dec 14, 2022
An iOS view-controller navigation management. No inherit, using one line code to integrate.

KGNavigationBar Example An iOS view-controller navigation management. No inherit, using one line code to integrate. 一个 iOS 控制器导航管理库. 无需继承, 一行代码即可实现集成。

VanJay 5 Sep 6, 2021
Powerful navigation in the Composable Architecture via the coordinator pattern

TCACoordinators The coordinator pattern in the Composable Architecture TCACoordinators brings a flexible approach to navigation in SwiftUI using the C

John Patrick Morgan 231 Jan 7, 2023
A wrapper for NavigationView and NavigationLink that makes programmatic navigation a little friendlier.

NavigatorKit A wrapper for NavigationView and NavigationLink that makes programmatic navigation a little friendlier. NavigatorKit is an opinionated wr

Gyuri Grell 2 Jun 16, 2022
Cordova/Phonegap plugin for launching today's most popular navigation/ride apps to navigate to a destination.

Launch Navigator Cordova/Phonegap Plugin Cordova/Phonegap plugin for launching today's most popular navigation/ride apps to navigate to a destination.

null 0 Oct 25, 2021
Simple custom navigation bar by swift

YoNavBarView Example To run the example project, clone the repo, and run pod install from the Example directory first. Requirements Installation YoNav

null 1 Nov 23, 2021