NavigationKit is a lightweight library which makes SwiftUI
navigation super easy to use.
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
Download and include the NavigationKit
folder and files in your codebase.
- iOS 14+
- Swift 5.3+
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.
Please read the README_0_3_11
on how to use v.0.3.11.
Import NavigationKit
into your View
import NavigationKit
Here's the list of the awesome features NavigationKit
has:
- pop to root
- built in Navigation Bars as view-modifiers (or build and use your own dream nav bar)
- works perfectly with
TabView
and.sheet
- works side by side with
NavigationView
(mix and match to your liking) - NO hacks, NO workarounds, NO custom solutions; pure
SwiftUI
code
In SwiftUI navigtion is handeled by the NavigationView
and NavigationLink
. At the moment these views have some limitations:
- we can't navigate back to root;
- customizing the
NavigationBar
is limited or it has to be done viaUINavigationBar.appearance()
(usingUIKit
π);
NavigationKit
integrates seamlesly into SwiftUI
becasue it is built on top of NavigationView
and NavigationLink
. It uses the same princiles while also improving on these two fileds:
- popping to root View
- having a custom navigation bar
To achieve these NavigationKit
introduces NavigationKitView
used together with NavigationKitLink
. Think of them as NavigationView
and NavigationLink
on steroids.
NavigationKitView
is a view that has all the behaviours belonging to the standard NavigationView
, but it adds the ability to pot to root.
To use it you have to consider two things:
- [1] you have to use it inside your
Root View
because you have to provide theisActive
binding. This binding is used to pop to root so it needs to be declared on theRoot View
. - [2] in order to achieve pop to root functionality you have to navigate with
NavigationKitLink
import NavigationKit
struct ContentView: View {
@State private var isPushedViewActive: Bool = false
var body: some View {
NavigationKitView(isActive: $isPushedViewActive) {
NavigationKitLink(isActive: $isPushedViewActive) {
ContentView2()
} label: {
Text("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 TextField
s 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()
}
}
}
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 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")
}
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.
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)
)
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)
)
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())
}
Presenting a modal is a bit diferent than pushing:
- create a
@State
variable for your view; - add a
Sheet
orFullScreenSheet
view with an optionalonDismiss
callback. You must add it to the view hierarchy. Don't worry they areEmptyView
s; - 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)
)
}
}
Here's how you can dismiss the modal:
- grab the
presentationMode
environment - 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 @Binding
s 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)
)
}
}
For a comprehensive Demo project check out: NavigationKitDemo
rebeloper.com / YouTube / Shop / Mentoring
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.