To save the current state in applications using SwiftUI, there are various mechanisms, for example, @SceneStorage
. However, due to the tight coupling to View
, this complicates the possibility of separating the logic of navigation and state saving. Additionally, due to SwiftUI bugs, the built-in mechanisms do not work in some cases and lead to various issues.
For navigation, use PersistentNavigator
with NavigatorScreenFactoryView
, which synchronizes the state of the navigator and navigation.
To store the current navigation state, simply use any storage that implements the NavigatorStorage
protocol. It doesn't matter if it's UserDefaults, a file, or encrypted storage.
Wrap everything in a NavigatorStoringView
, which will save the current navigation state whenever any changes occur.
typealias Navigator = PersistentViewNavigator<Destination, TabTag, SheetTag>
struct WindowView: View {
let navigatorStorage: DefaultsNavigatorStorage
let navigator: Navigator
var body: some View {
NavigatorStoringView(navigator: navigator, storage: navigatorStorage) {
NavigatorScreenFactoryView(
navigator: navigator,
buildView: { destination, navigator in
switch destination {
case .root: RootScreenView()
case .details: DetailsScreenView()
case .more: MoreScreenView()
}
},
buildTab: { tabTag in
switch tabTag {
case .first: Label("Tab 1", systemImage: "pencil.circle")
case .second: Label("Tab 2", systemImage: "square.and.pencil.circle")
case .none: EmptyView()
}
},
getDetents: { sheetTag in
switch sheetTag {
case .first: ([.medium, .large], .visible)
case .none: nil
}
}
)
}
}
}
More detailed information can be found in the example project.
-
push
. Pushes a newDestination
onto theNavigationStack
. -
pop
. Pops the top-most view off theNavigationStack
, returning to the previous view. This is equivalent to tapping the back button in a standard navigation interface. -
pop(to:)
. Pops to a specifiedDestination
in theNavigationStack
. Useful for skipping intermediate views and jumping directly to a particular destination. -
popToRoot
. Pops all views off theNavigationStack
until the root view is reached. This resets the navigation stack to the initial state. -
replace(root:isPopToRoot:)
. Replaces the current root view in theNavigationStack
with a new rootDestination
. Optionally, you can choose to pop to the root after replacement. -
present
. Presents a new view as asheet
orfullScreenCover
. This is used to modally display views on top of the current screen. -
dismissTop
. Dismisses the top-most presentedsheet
orfullScreenCover
, returning to the previous screen. This is commonly used to close modally presented views. -
dismiss(to:)
. Dismisses presentedsheets
orfullScreenCovers
until a specifiedDestination
orid
is reached. This allows for more controlled dismissal in cases with multiple modal presentations. -
closeToInitial
. Dismisses all presentedsheets
andfullScreenCovers
, and resets the initialNavigationStack
's navigation path to its root state. This is useful for completely resetting the navigation flow. -
close(to:)
. Attempts to navigate to and close all modally presented views, while navigating to a specified targetDestination
. This can be used to programmatically close views and move to a specific part of the navigation flow. -
close(where:)
. Similar toclose(to:)
, but allows for specifying a predicate to determine whichDestination
to navigate to and close. This offers more flexibility in choosing the navigation target. -
onReplaceInitialNavigator
. A callback that is triggered when the initialNavigator
needs to be replaced with a new one. This allows for dynamic changes in the navigator setup. -
currentTab
. A variable that holds the current tab in aTabView
. This allows for both getting and setting the active tab programmatically. -
open(url:)
. Opens a given URL. -
open(window:)
. Opens a new window with the specified identifier. -
dismiss(window:)
. Dismisses a window with the specified identifier. -
pass(action:)
. Dispatches an action to the environment. Sends the provided action, making it available to any view that has registered an external action handler via.handle(_)
or.handle<T>(_)
.
In addition to the Navigator
's methods, you can also use SwiftUI's standard NavigationLink
for navigation. This integrates seamlessly with the navigator as long as your destinations conform to PersistentDestination
for PersistentNavigator
or Hashable
for TypedNavigator
.
Example:
NavigationLink(value: Destination.player(file.url)) {
FileView(file: file)
}
struct FeatureDetailsScreenView: View {
@Environment(\.persistentNavigator) var navigator
var body: some View {
VStack(spacing: 16) {
Text("Current: Some separate feature flow details")
.multilineTextAlignment(.center)
Button("More") {
navigator.present(.view(Destination.feature(.more)))
}
Button("Pop") {
navigator.pop()
}
Button("Dismiss") {
navigator.dismissTop()
}
Button("Close all") {
navigator.closeToInitial()
}
}
}
}
struct FeatureDetailsScreenView: View {
@Environment(\.baseNavigator) var navigator
var body: some View {
VStack(spacing: 16) {
Text("Current: Some separate feature flow details")
.multilineTextAlignment(.center)
Button("More") {
navigator.present(.view(Destination.feature(.more)))
}
Button("Pop") {
navigator.pop()
}
Button("Dismiss") {
navigator.dismissTop()
}
Button("Close all") {
navigator.closeToInitial()
}
}
}
}
// If you need a concrete type of navigator
extension EnvironmentValues {
var navigator: Navigator { persistentNavigator as! Navigator }
}
struct FeatureDetailsScreenView: View {
@Environment(\.navigator) var navigator
var body: some View {
VStack(spacing: 16) {
Text("Current: Some separate feature flow details")
.multilineTextAlignment(.center)
Button("More") {
navigator.present(.view(Destination.feature(.more)))
}
Button("Pop") {
navigator.pop()
}
Button("Dismiss") {
navigator.dismissTop()
}
Button("Close all") {
navigator.closeToInitial()
}
}
}
}
More detailed information can be found in the example project.
Volodymyr Andriienko, [email protected]
VAPersistentNavigator is available under the MIT license. See the LICENSE file for more info.