Существует множество статей, описывающих стек навигации, который был представлен в iOS 16, но большинство из них в значительной степени повторяют то, что говорится в документации Apple, и похожи на образец приложения Colors, которым поделилась Apple. Хотя это хорошо для понимания основ, этого далеко недостаточно, чтобы понять, как включить навигационный стек в реальное приложение.
К концу этого руководства у нас будет подход на основе перечислений с конкретным примером, объясняющим, как включить глубокую навигацию с ожидаемым поведением представления Tab
.
Итак, давайте углубимся в это, создав представление Tab
:
struct TabScreenView: View { //enum for Tabs, add other tabs if needed enum Tab { case home, goals, settings } @State private var selectedTab: Tab = .home var body: some View { TabView(selection: $selectedTab) { HomeScreen() .tabItem { Label("Home", systemImage: "house") } .tag(Tab.home) GoalsScreen() .tabItem { Label("Goals", systemImage: "flag") } .tag(Tab.goals) SettingsScreen() .tabItem { Label("Settings", systemImage: "gear") } .tag(Tab.settings) } } }
У нас есть базовый TabView
, который позволяет переключаться между тремя экранами с помощью вкладок. Я добавил несколько фиктивных данных в несколько представлений и связал их с главным экраном, так что на данный момент у нас есть вот что:
Обратите внимание: когда я нахожусь во втором дочернем представлении, и когда я нажимаю кнопку вкладки, я не возвращаюсь на домашнюю страницу, поэтому мне приходится дважды нажимать кнопку «Назад», чтобы вернуться на главную.
Это может быть несколько нажатий, если приложение большое и ваши пользователи не скажут вам за это спасибо.
Большинство панелей вкладок имеют две функции при нажатии на значок вкладки: всплывающее окно в корневой каталог и прокрутку вверх. Вы можете перейти к следующему абзацу, если знаете, что он означает.
- Перейти к корневому представлению. Независимо от того, насколько глубоко вы находитесь на вкладке, нажатие на значок вкладки приведет вас к домашнему/корневому представлению. Например, у вас есть приложение для просмотра фильмов с панелью поиска на главном экране, которая показывает результаты фильма на втором представлении, и вы можете нажать на один из результатов, и оно перейдет к третьему представлению, в котором показаны подробности фильма. Нажатие на значок вкладки должно вернуть вас на панель поиска.
- Прокрутить вверх достаточно легко. Если вы находитесь на главном экране и нажимаете значок вкладки, он должен прокрутиться вверх.
Теперь, как объединить оба эти поведения? Нам нужно знать несколько вещей:
- Когда нажата иконка вкладки и является ли нажатый значок активной в данный момент вкладкой.
- Следует ли перейти в корень или прокрутить вверх
- И, наконец, как.
Давайте рассмотрим эти шаги по одному.
Щелчок по значку вкладки → Вопреки первой мысли, которая приходит в голову, onTapGesture
не работает со значками вкладок, что немного странно. Поэтому мы идем другим подходом. TabView
можно управлять, просто установив привязку selectedTab
, так что это наш путь. Давайте посмотрим это в коде:
{ ...code TabView(selection: tabSelection()) { } ...code } extension TabScreenView { private func tabSelection() -> Binding<Tab> { Binding { //this is the get block self.selectedTab } set: { tappedTab in if tappedTab == self.selectedTab { //User tapped on the currently active tab icon => Pop to root/Scroll to top } //Set the tab to the tabbed tab self.selectedTab = tappedTab } } }
Мы создаем функцию tabSelection
, которая действует как наш посредник и отвечает за получение и установку привязки selectedTab
.
Это решает нашу первую проблему, и мы знаем, когда нажата вкладка и где мы находимся, когда это происходит. Для решения второй проблемы нам придется разобраться с навигацией.
Теперь NavigationStack
и TabView
работают вместе, обеспечивая удобство взаимодействия с пользователем. Например, если вам нужно вернуть пользователя в корневое представление, один из способов — передать привязки из представления вкладок и переключить их на всплывающее представление в корневом режиме. Но это будет все загромождать, и вам придется передавать несколько привязок каждому представлению. Не лучший способ сделать это.
Вместо этого мы используем перечисления и стек навигации SwiftUI.
Вот базовый NavigationStack
без перечислений:
//Use an array of Integers with each integer pointing to a destination view @State private var path = [Int]() //or use @State private var path = NavigationPath() NavigationStack(path: $path) {//Trailing closure for root: NavigationLink(value: 1) { //Based on what this value is the navigation Destination shall be decided Text("Click me to navigate") //Label } ...other code //Our switching happens here .navigationDestination(for: Int.self) { value in //Based on your need, you may add other conditions or better use a switch if value == 1 { ChildView() } } }
Позвольте мне объяснить больше для тех, кто никогда не видел этого раньше.
Стек навигации нуждается в привязке к NavigationPath
, поскольку это похоже на его источник истины.
Этот путь сообщает стеку, где мы находимся, и каждый экран, который вы добавляете в навигацию вашего приложения, добавляется к этому пути.
Итак, если вы очистите путь, вы вернетесь в корень пути, в корневое представление.
Теперь давайте создадим несколько перечислений. Ради экономии времени я создам только стек домашней навигации.
//an enum for each Tab that tracks the views of the Tab enum HomeNavigation: Hashable { case child, secondChild } //Declare navigationStacks for each of the tabs @State private var homeNavigationStack: [HomeNavigation] = [] //Pass this state to HomeScreen HomeScreen(path: $homeNavigationStack) .tabItem { Label("Home", systemImage: "house") } .tag(Tab.home)
Теперь этот homeNavigationStack
будет отслеживать, на каком экране мы находимся на вкладке «Главная». Мы очищаем этот массив и возвращаемся к корню. Нам нужно будет создать стеки для каждой из вкладок.
Это позволяет четко обозначить и разделить логику каждой из вкладок.
После включения навигации в наш HomeScreen
мы получаем следующее:
struct HomeScreen: View { //This path can come from environmene object View Model @Binding var path: [HomeNavigation] var body: some View { NavigationStack(path: $path) { //Specify the enum of the screen you want as the destination view NavigationLink(value: HomeNavigation.child) { Text("Click me to navigate") } //This is always declared at the root .navigationDestination(for: HomeNavigation.self) { screen in switch screen { case .child: ChildView() case .secondChild: SecondChildView() } } .navigationTitle("Home") } } }
Примечание. Swift не позволит вам передать путь как есть. Должно быть
Hashable
!!! Обратите внимание, что я добавил соответствиеHashable
для перечисленияHomeNavigation
выше.
Теперь, когда мы настроили навигацию, с каждым экраном будет связано перечисление, которое Swift автоматически добавляет к массиву путей при навигации.
Все, что нам нужно сделать, это очистить это, и мы вернемся к корню.
Давайте обновим нашу функцию tabSelection
:
private func tabSelection() -> Binding<Tab> { Binding { //this is the get block self.selectedTab } set: { tappedTab in if tappedTab == self.selectedTab { //User tapped on the currently active tab icon => Pop to root/Scroll to top if homeNavigationStack.isEmpty { //User already on home view, scroll to top } else { //Pop to root view by clearing the stack homeNavigationStack = [] } } //Set the current tab to the user selected tab self.selectedTab = tappedTab } }
Прокрутить вверх легко. Добавьте
State
, передайте егоHomeScreen
и добавьтеonChange
, который проверяет, верна ли эта привязка, и прокручивает до идентификатора. Вот ветка Переполнение стека на эту тему.
Наконец, давайте поговорим о передаче чего-либо между представлениями. По мере роста нашего приложения мы начинаем создавать больше подпредставлений.
Но как вы что-то передадите, если пункт назначения навигации находится в корне, где этих аргументов не существует?
Рассмотрим структуру Person
:
struct Person: Hashable { let name: String let lastName: String }
Предположим, что ChildView
необходимо передать этот Person
второму дочернему элементу. Мы могли бы получить список людей и отобразить его в дочернем представлении, а второй дочерний элемент — это подробное представление о человеке.
Сейчас, в Home
, мы понятия не имеем, каких людей мы поймали или нет. Давайте решим эту распространенную, но важную проблему.
Обновите перечисление HomeNavigation
следующим образом:
enum HomeNavigation: Hashable { case child, secondChild(Person) }
Мы просто добавили связанный тип, и это почти все. Теперь в нашем ChildView
мы можем довольно легко отправлять данные типа Person
.
struct ChildView: View { let person = Person(name: "Akshay", lastName: "Mahajan") var body: some View { VStack { Text("Child View") //NOTICE THE PERSON ADDED TO THE VALUE NavigationLink(value: HomeNavigation.secondChild(person)) { Text("Click to enter second view") } } .navigationTitle("Child") } }
И наш navigationDestination
обновляется следующим образом:
.navigationDestination(for: HomeNavigation.self) { screen in switch screen { case .child: ChildView() case .secondChild(let person): SecondChildView(person: person) } }
Теперь наше представление вкладок ведет себя так, как ожидалось, и поток приложения выглядит следующим образом:
Это меня очень огорчило. Посмотрите мой пример приложения на GitHub.
Спасибо за прочтение.