• Stars
    star
    173
  • Rank 220,124 (Top 5 %)
  • Language
    Swift
  • Created about 4 years ago
  • Updated almost 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

🧭 SwiftUI navigation done right

🧭 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.

More Repositories

1

AlertKit

🚨 SwiftUI alerts (and action sheets) done right
Swift
76
star
2

WKView

🀩 WKWebView for SwiftUI with Delegates 🀯
Swift
63
star
3

SparkUI

πŸ§™β€β™‚οΈBuild iOS apps at lightning speed
Swift
53
star
4

ImagePickerView

πŸŒ‡ PHPickerViewController / UIImagePickerController for SwiftUI
Swift
37
star
5

DuckUI

Swift
24
star
6

SwiftUICloudKit

Swift
19
star
7

HUDKit

πŸͺ§ The missing SwiftUI HUD
Swift
17
star
8

SwiftUIPlus

Swift
15
star
9

FirebaseService

Swift
11
star
10

SwiftUIMVVM

Swift
9
star
11

NavigationViewModifier

Swift
7
star
12

SearchBarView

πŸ”Ž The missing UISearchBar from SwiftUI
Swift
7
star
13

AVCamera

Swift
7
star
14

SideMenu

Swift
6
star
15

SwiftUISignInWithApple

Swift
6
star
16

Dot

Swift
6
star
17

NavigationStackExampleDemo

Swift
6
star
18

MCVvsMVVM

Swift
5
star
19

NavigationKitDemo

Swift
4
star
20

MagicUI

Swift
4
star
21

ShareSheetView

πŸ“€ The missing UIActivityViewController for SwiftUI
Swift
4
star
22

SwiftUIViewTextStack

Swift
4
star
23

GoogleMapsDemo

Swift
4
star
24

SwiftUIMongoDB

Swift
4
star
25

Stax

πŸ† Best way to build UI
Swift
3
star
26

ImagePickerViewDemo

Swift
3
star
27

SwiftUIAlerts

Swift
3
star
28

SwiftUICoordinator

Swift
3
star
29

SwiftUIGeometryReader

Swift
3
star
30

LocalAndPushNotifications

Swift
3
star
31

NetworkState

Internet Connectivity for SwiftUI with NWPathMonitor
Swift
3
star
32

AlernateIcons

Swift
2
star
33

ColorSchemeKit

Force set a Color Scheme for your SwiftUI app
Swift
2
star
34

SwiftUIDynamicList

Swift
2
star
35

Secrets

Swift
2
star
36

SwiftUIDarkMode

Swift
2
star
37

ShareSheetViewDemo

Swift
2
star
38

SwiftUINavigation

Swift
2
star
39

SwiftUICoreData

Swift
2
star
40

WKViewDemo

Swift
2
star
41

PullToRefreshView

Swift
2
star
42

CoreDataSwiftUI4

Swift
2
star
43

FirebaseTwitterDemo

Swift
2
star
44

SwiftUI3Fun

Swift
2
star
45

SwiftUITabView

Swift
2
star
46

GeometryStack

Swift
2
star
47

CroppedPhotosPickerDemo

Swift
2
star
48

BuildAnything

Swift
2
star
49

SwiftUIRoll

Swift
2
star
50

SwiftUIAnimation

Swift
1
star
51

TransformingViews

Swift
1
star
52

SwiftUITextFiels

Swift
1
star
53

UIKitSignInWithApple

Swift
1
star
54

TableViewExample

Swift
1
star
55

ShadowWithCornerRadiusInSwift5

Swift
1
star
56

SwiftUICombine

Swift
1
star
57

SecretsReader

C
1
star
58

RemoteConfig

Swift
1
star
59

SwiftUIinUIKitwithDuckUI

Swift
1
star
60

SNavigatorDemo

Swift
1
star
61

SwiftUIStateBinding

Swift
1
star
62

SwiftUIModifiersInUIKit

Swift
1
star
63

CoreDataFun1

Swift
1
star
64

InfoPlistParser

Swift
1
star
65

CountdownTimerUIKitDemo

Swift
1
star
66

CustomFrameworkDemo

C
1
star
67

VisualDebugInSwift5

Swift
1
star
68

QonversionExample

Swift
1
star
69

OneDot

The missing stuff from SwiftUI
Swift
1
star
70

NavigationStepDemoOld

Swift
1
star
71

SwiftUIAlertActionSheet

Swift
1
star
72

SwiftUIElements

Swift
1
star
73

SwiftUIBinding

Swift
1
star
74

MyDemoSP

Swift
1
star
75

kindaSwiftUI

It's kinda SwiftUI, but better. The trial is full-featured and there's no expiry date, but without a subscription kindaSwiftUI sleeps from time to time. You may consider subscribing for 'SwiftUI Nation' to keep kindaSwiftUI awake 24/7 πŸ‘‡
C++
1
star
76

ProgressButtonDemo

Swift
1
star
77

BFShared

Swift
1
star
78

UserDefaultsUIKit

Swift
1
star
79

NetworkMonitor

Swift
1
star
80

RangeSlider

Swift
1
star
81

EnumNavigation

Swift
1
star
82

SwiftUINavigator

Swift
1
star