SUINavigation
Simple SwiftUI Navigation component. Alternate NavigationStack with supporting from iOS 14. We are fixing all know Apple bugs and performance drop. Compatible with Routing, Coordinator and each other architecture patterms. This navigation has functions of getting and applying the URL which allows you to organize deep links without special costs.
Install / Use
/learn @ozontech/SUINavigationREADME
SUINavigation
Overview
Simple navigation framework for SwiftUI. Alternative NavigationStack with support for iOS 14, Apple bug fixes and better features. Compatible with Routing, Coordinator and each other architecture patterns. This navigation framework has functions of getting and applying the URL which allows you to organize deep links without special costs. In addition, the package contains a separate public test framework SUINavigationTest for testing navigation with unit tests and snapshot tests. We care about quality and performance what why we have UI and Unit tests.
Motivation
Now Developers have standard navigation framework SwiftUI. "Correct" navigation features were introduced since iOS 16 as NavigationStack, but developers can not use that becase should support a iOS 14.x as target commonly. Now we have solutions to backport NavigationStack: NavigationBackport but it's too bold and different from the declarative approach. We want a simpler interface. In addition, the NavigationStack and NavigationBackport havn't many functions such as skip and each others. Functions append and replace from SUINavigation with URL allows store and backup navigation state without special costs. Also allows you to use deep links as an additional feature in this SwiftUI navigation component. If you want to use microfeature-architecture you can inject views to your modules closed by value object type.
Features
- [x] Full support SwiftUI, has declarative style.
- [x] Supporting iOS 14, iOS 15, iOS 16, iOS 17, iOS 18, iOS 26.
- [x] Target switching between NavigationView and NavigationStack.
- [x] Fixing known Apple bugs of standart SwiftUI navigation components.
- [x] Has pop, popTo, skip, isRoot and each other functions.
- [x] Works with URL: simple supporting the deep links.
- [x] Multy-module supporting (views injecting).
- [x] Contains unit and snapshot tests framework.
- [x] UI tests full coverage.
- [x] Performance concern.
Installation
Note that the framework consists of several libraries. The SUINavigation library is needed to use navigation in the application, and SUINavigationTest is needed to cover unit tests, if you try to use SUINavigationTest to compile the application, you will most likely encounter linking errors.
Swift Package Manager (SPM)
Once you have your Swift package set up, adding SUINavigation as a dependency is as easy as adding it to the dependencies value of your Package.swift. And next step: add library to targets. For the application target you should use SUINavigation library and for tests target you should use SUINavigationTest library.
dependencies: [
.Package(url: "https://github.com/ozontech/SUINavigation.git", majorVersion: 1)
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "SUINavigation", package: "SUINavigation"),
],
),
.testTarget(
name: "YourTests",
dependencies: [
.product(name: "SUINavigationTest", package: "SUINavigation"),
],
),
],
From XCode
You can integrate SUINavigation into your project from Xcode.
To do this you need to go to Project of the root of Project Navigator, then choose tab Package Dependencies and when you click add enter the path to this repository. You will then be prompted to add certain libraries to the targets, it is important to choose the right targets based on your usage goals, as mentioned at the beginning of the installation article. Note that the example below shows adding to the application target and test target. If you specify the wrong targets, you may experience linking errors.

If you didn't use tests, then for SUINavigationTest in screen above you need setup Add to Target to None.
Build & test
Bash script
For tests on popular iOS versions you just call script:
sh Scripts/release.sh
The same script can prepare release with detection version from CHANGELOG and you can use it for tag new version. For the creating release notes I use GL, installed and setup and call next script:
brew install gh
gh auth login
sh Scripts/releasenotes.sh
Manually
Just open Example/NavigationExample/NavigationExample.xcodeproj from Xcode and you can build, test this from IDE.
Using
Use NavigationStorageView instead of NavigationView or NavigationStack.
In parent view use modifiers .navigation(..) with string id param or without (for using struct name) in addition features as in the code below.
- Note that
.navigation(..)applies to all content in the view being navigated from. This modifier should not be allowed to apply to child elements of that view, such as childs of ListView, LazyView and each others. Better use.navigation(..)below of all content of that view as in the code below.
import SwiftUI
import SUINavigation
struct RootView: View {
// True value trigger navigation transition to FirstView
@State
private var isShowingFirst: Bool = false
var body: some View {
NavigationStorageView{
VStack {
Text("Root")
Button("to First"){
isShowingFirst = true
}
}.navigation(isActive: $isShowingFirst){
FirstView()
}
}
}
}
struct FirstView: View {
// Not null value trigger navigation transition to SecondView with this value, nil value to dissmiss to this View.
@State
private var optionalValue: Int? = nil
var body: some View {
VStack(spacing: 0) {
Text("First")
Button("to Second"){
optionalValue = 777
}
}.navigation(item: $optionalValue, id: "second"){ item in
// item is unwrapped optionalValue where can used by SecondView
SecondView(number: item)
}
}
}
struct SecondView: View {
@State
private var isShowingLast: Bool = false
var body: some View {
VStack(spacing: 0) {
Text("Second")
Button("to Last"){
isShowingLast = true
}
}.navigation(isActive: $isShowingLast, id: "last"){
SomeView()
}
}
}
struct SomeView: View {
// This optional everywhere, because in a test can use NavigationView without navigationStorage object
@OptionalEnvironmentObject
private var navigationStorage: NavigationStorage?
// Standard feature of a dissmiss works too. Swipe to right works too.
@Environment(\.presentationMode)
private var presentationMode
var body: some View {
Button("Go to First") {
// You should use struct for navigate, because it determinate without id
navigationStorage?.popTo(FirstView.self)
}
Button("Go to SecondView") {
// You should use id for navigate to SecondView, because it determinate as id
navigationStorage?.popTo("second")
}
Button("Go to Root") {
navigationStorage?.popToRoot()
}
Button("Skip First") {
navigationStorage?.skip(FirstView.self)
}
Button("Skip Second") {
navigationStorage?.skip("second")
}
}
}
Test of Navigation
SUINavigation is a SwiftUI navigation component that includes SUINavigationTest test library, which allows you to cover your views navigation rooting with Unit and Snapshot tests without use Router or Coordinator architecture approach.
More details about why this is needed and how to implement it are written in a separate article SUINavigationTest.
The Next example just shows how to write tests:
import SUINavigationTest
final class NavigationExampleTests: XCTestCase {
/// Unit Test
func testView1ToView2() throws {
let view1 = View1()
test(sourceView: view1, destinationView: View2.self) {
view1.triggerValue = "trigger"
} destination: { view2 in
XCTAssertEqual(view2.inputValue, "trigger")
}
}
/// Snapshot Test
func testAllItemsOfTheRoot() throws {
let rootView = RootView()
try assertItemsSnapshot(rootView)
}
}
Deeplinks supporting
SUINavigation has functions of getting and applying the URL which allows you to organize deep links without special costs. Modifier .navigationAction identical .navigation, but support navigate by append or replace from URL (URI). If you want custom navigate or use presentation type of navigation (alert, botomsheet, fullScreenCover, TabBar, etc) you can use part of .navigationAction as .navigateUrlParams. Modifiers .navigationAction as .navigateUrlParams have addition sets of params for customisation an URL representation.
More about Deeplinks in separated article page Deeplinks.
The Next example just shows how to navigate from url:
import SwiftUI
import SUINavigation
struct SomeView: View {
@OptionalEnvironmentObject
private var navigationStorage: NavigationStorage?
@State
private var optionalValue: Int? = nil
let url = "second?secondValue=777"
var body: some View {
VStack() {
Text("Some")
Button("to Second from URL"){
navigationStorage.append(url)
}
}.navigationAction(item: $optionalValue, id: "second", paramName: "secondValue") { item in
SecondView(number: item)
}
}
}
Features of Nested Navigation
Since NavigationStack don't support nested NavigationStack it affected to NavigationStorageView too.
