43. Пример работы с NavigationStack
В этой статье посмотрим на вариант построения навигации с использованием NavigationStack (iOS 16+). Для наглядности адаптируем под NavigationStack код из предыдущей статьи, там же можно посмотреть демо-видео с результатом.
Наименования всех файлов в новом примере поменял с NV* на NS*, а верстка и почти вся логика остались без изменений.
Посмотрим на основные отличия двух инструментов для навигации.
1) Возвращаемся к корневому экрану (pop to root)
NavigationStack позволяет очень легко сделать pop to root, просто заменив путь пустым значением:
@MainActor
final class NSAppViewModel: ObservableObject {
/// Хранит актуальный путь навигации для конкретного `NavigationStack`.
/// В нашем примере стек всего один - внутри `NSRootScreen`
@Published var path = NavigationPath()
// Остальной код ...
func popToRoot() {
path = .init() // <- вот и весь popToRoot`
}
}
2) Выполняем программную навигацию
Под "программной навигацией" я подразумеваю переходы на следующие экраны без явного использования NavigationLink, лейбл которого по умолчанию поддерживает нажатие и выполнение перехода на указанный экран.
Для управления навигацией достаточно обратиться к нужному стеку навигации через path, который в нашем примере находится внутри NSAppViewModel. и сделать append или другую нужную операцию из общего списка (документация), например:
private extension NSMainScreen {
var profileButton: some View {
Button {
// Переходим в "Профиль"
viewModel.path.append(NavigationDestination.profile)
} label: {
Label("Профиль", systemImage: "person")
}
}
var bookmarksButton: some View {
Button {
// Переходим в "Закладки"
viewModel.path.append(NavigationDestination.bookmarks)
} label: {
Label("Закладки", systemImage: "bookmark")
}
}
var settingsButton: some View {
Button {
// Переходим в "Настройки"
viewModel.path.append(NavigationDestination.settings)
} label: {
Label("Настройки", systemImage: "gear")
}
}
}
3) Удаляем ненужный код
Больше не нужно держать в бэкграунде экрана NavigationLink, поэтому такие строчки можно целиком удалить:
.background( NavigationLink( destination: destinationView, isActive: $navigationDestination.mappedToBool() ) )
4) Обновляем метод для обновления appState
Чтобы наша первая реализация со сменой состояния корневого экрана работала корректно, нужно внутри вьюмодели (NSAppViewModel) вовремя возвращаться к корневому экрану при помощи popToRoot(), пример:
/// Обрабатывает действие
func process(action: AppAction) {
switch action {
case .performAuth:
appState = .auth
isAuthorized = true
case .performLogout:
popToRoot() // <- тут сбросили стек
appState = .notAuth
isAuthorized = false
case let .showError(string):
popToRoot() // <- тут сбросили стек
appState = .error(string)
case let .closeError(stayAuthorized):
appState = stayAuthorized ? .auth : .notAuth
isAuthorized = stayAuthorized
if !stayAuthorized { popToRoot() } // <- тут сбросили стек
}
}
func popToRoot() {
path = .init()
}
5) Обрабатываем навигацию в корневом экране
NavigationStack появился вместе с navigationDestination, вот как его нужно применить в нашем случае:
struct NSRootScreen: View {
@StateObject private var viewModel = NSAppViewModel()
var body: some View {
NavigationStack(path: $viewModel.path) {
contentView
.navigationDestination(
for: NSMainScreen.NavigationDestination.self
) { destination in
switch destination {
case .profile: NSProfileScreen()
case .settings: NSSettingsScreen()
case .bookmarks: NSBookmarksScreen()
}
}
}
.environmentObject(viewModel)
}
}
Вот и все! Переход на новую навигацию успешно завершен.
Если очень хочется применять что-то похожее на NavigationStack на старых версиях iOS, то можно поискать бэкпорты, в качестве примера вот один из вариантов. Сам не использовал, но кому-то помогает.
Важный нюанс
При поддержке разных вариантов навигации в одном проекте могут вылезать самые разные баги, включая незакрывающиеся/неоткрывающиеся экраны, падения приложения при открытии вложенных NavigationStack и т.д.
Во избежание таких проблем можно:
- Тщательно тестировать и писать обходные решения (то еще занятие)
- Не использовать
NavigationStackпри поддержке минимальной iOS < 16, то есть откатиться наNavigationView - Использовать навигацию на
UIKit, например, на координаторах, которых на гитхабе много (недавно именно с координаторов я переносил всю навигацию в рабочем проекте на SwiftUI иNavigationView)
Код для этой статьи можно посмотреть тут, а другие статьи - тут.