Первое с чем вы сталкиваетесь при написании мобильного приложения, это переходы с одного экрана на другой и обратно (в случае простого приложения), а так же многоуровневые переходы и моментальный возврат на первый экран (back to root view). Всё это мы разберём на примерах в данной статье, ну а для начала немного углубимся в теорию.

Что такое NavigationView?

Представьте, что перед вами коробка (NavigationView) в которой много мячиков (View) и эти мячики могут соприкасаться друг с другом (переход с View на View) только в рамках этой коробки. 

Но есть так же и случаи, когда внутри коробки, помимо шариков, есть ещё одна коробка поменьше, которая содержит другие маленькие шарики (история про .sheet и .fullScreenCover - модальные окна). Это так же разберём ниже в статье.

Содержание статьи

На схеме отражены варианта использования NavigationView:

Пример 1: Последовательные переходы от View к View на несколько уровней в глубь.
Пример 2: Множественные переходны на разные View с первого View.
Пример 3: Объявление нового NavigationView при использовании модальных окон.
Пример 4 (доп.): Аналогия "Примера 1" с переходом с последнего View сразу на первый (root View)

Далее разберём каждый из примеров более детально.

Пример 1: Последовательные переходы от View к View на несколько уровней в глубь

На первом View мы объявляем NavigationView в который заворачиваем ссылку (NavigationLink) на следующий View.

Здесь единственным отличием первого View от остальных, является объявление NavigationView, так как в остальных View его объявлять уже не требуется, ввиду применения наследования (вспоминаем "коробку с шариками" из начала статьи)

// первый экран
struct View1: View {
    var body: some View {
      // объявляем NavigationView единожды на первом экране
        NavigationView {
            NavigationLink {
                View1_1()
            } label: {
                Text("Переход на View1_1")
            }
            .navigationTitle("View1")
        }
    }
}

// второй экран
struct View1_1: View {
    var body: some View {
        NavigationLink {
            View1_2()
        } label: {
            Text("Переход на View1_2")
        }
        .navigationTitle("View1_1")
    }
}

// третий экран
struct View1_2: View {
    var body: some View {
        Text("Последний экран")
    }
}
GIF с примером

Пример 2: Множественные переходы на разные View с первого View

В отличии от "Примера 1" здесь покажу два варианта. Один с использованием Binding свойства и второй с использованием teg'ов.

Прошу обратить внимание, что в "Примере 2" можно так же использовать вариант из "Примера 1". А Binding и tag описаны для понимания всех возможных вариантов перехода.

Вариант кода с применением подхода из "Примера 1"
// первый экран
struct View2: View {
    var body: some View {
        NavigationView {
            VStack {
                
                NavigationLink {
                    View2_1()
                } label: {
                    Text("Переход на View2_1")
                }
                
                NavigationLink {
                    View2_2()
                } label: {
                    Text("Переход на View2_2")
                }
            }
            .navigationTitle("View2")
        }
    }
}

// второй экран (открывающийся по ссылке с первого)
struct View2_1: View {
    var body: some View {
        Text("View2_1")
            .navigationTitle("View2_1")
    }
}

// третий экран (открывающийся по ссылке с первого)
struct View2_2: View {
    var body: some View {
        Text("View2_2")
            .navigationTitle("View2_2")
    }
}

Вариант с Binding:

Касательно NavigationView, здесь смысл тот же что и в "Примере 1" (объявляем NavigationView в который заворачиваем ссылку (NavigationLink) на следующий View), отличие только в количестве этих самых ссылок (NavigationLink). Ну и для активации ссылки выбираем вариант с использованием Binding свойства.

Итогом получаем переходы на несколько разных View с одного.

// первый экран
struct View2: View {
    
    @State private var firstViewIsOn = false
    @State private var secondViewIsOn = false
    
    var body: some View {
        NavigationView {
            VStack {
              // ссылка на новое View, которая активируется при 
              // изменении свойства firstViewIsOn
                NavigationLink(isActive: $firstViewIsOn) {
                    View2_1()
                } label: {
                    Button {
                        firstViewIsOn.toggle()
                    } label: {
                        Text("Переход на View2_1")
                    }
                }
                
              // ссылка на новое View, которая активируется при 
              // изменении свойства secondViewIsOn
                NavigationLink(isActive: $secondViewIsOn) {
                    View2_2()
                } label: {
                    Button {
                        secondViewIsOn.toggle()
                    } label: {
                        Text("Переход на View2_2")
                    }
                }
            }
            .navigationTitle("View2")
        }
    }
}

// второй экран (открывающийся по ссылке с первого)
struct View2_1: View {
    var body: some View {
        Text("View2_1")
            .navigationTitle("View2_1")
    }
}

// третий экран (открывающийся по ссылке с первого)
struct View2_2: View {
    var body: some View {
        Text("View2_2")
            .navigationTitle("View2_2")
    }
}

Вариант с teg'ами:

Всё происходит по аналогии варианта с Binding свойством за исключением того, что свойство одно, а в NavigationLink добавляется teg по которому происходит определение, какую из ссылок активировать.

// первый экран
struct View2: View {
    
    @State private var tagSelection: String? = nil
    
    var body: some View {
        NavigationView {
            VStack {
              // ссылки на View с привязкой к teg'у
                NavigationLink(destination: View2_1(), tag: "view1", selection: $tagSelection) { EmptyView() }
                NavigationLink(destination: View2_2(), tag: "view2", selection: $tagSelection) { EmptyView() }
                
                Button {
                  // сообщаем объявленному свойству значение teg'а
                  // на основании которого будет активированн та или
                  // иная ссылка
                    tagSelection = "view1"
                } label: {
                    Text("Переход на View2_1")
                }

                Button {
                    tagSelection = "view2"
                } label: {
                    Text("Переход на View2_2")
                }
            }
            .navigationTitle("View2")
        }
    }
}

// второй экран (открывающийся по ссылке с первого)
struct View2_1: View {
    var body: some View {
        Text("View2_1")
            .navigationTitle("View2_1")
    }
}

// третий экран (открывающийся по ссылке с первого)
struct View2_2: View {
    var body: some View {
        Text("View2_2")
            .navigationTitle("View2_2")
    }
}
GIF с примером

Пример 3: Объявление нового NavigationView при использовании модальных окон

При использовании модификаторов типа .sheet или .fullScreenCover вы, так сказать начинаете всё с начала, а точнее, чтобы получить навигацию на самом модальном View, вам необходимо заново объявить NavigationView, после чего цикл всех последующих View начнётся заново в рамках нового NavigationView.
Вспомните про "коробку в коробке" о которой я писал в начале статьи.

Разберём такой подход на небольшом примере:

struct View3: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: {
                View3_1()
            }, label: {
                Text("Переход на View3_1")
            })
            .navigationTitle("View3")
        }
    }
}

struct View3_1: View {
    
    @State private var activateModalView = false
    
    var body: some View {
        Button(action: {
            activateModalView.toggle()
        }, label: {
            Text("Модальное окно")
        })
            .navigationTitle("View3_1")
      			// модальное окно
            .sheet(isPresented: $activateModalView) {
              // объявляем новый NavigationView, без которого
              // NavigationLink на View3_2 работать не будет
                NavigationView {
                    View3_2()
                }
            }
    }
}

struct View3_2: View {
    var body: some View {
        NavigationLink(destination: {
            Text("Новое окно, на которое перешли внутри модального")
        }, label: {
            Text("Переход внутри модального окна")
        })
            .navigationTitle("View3_2")
    }
}
GIF с примером

Пример 4 (доп.): Аналогия "Примера 1" с переходом с последнего View сразу на первый (root View)

Ну и на последок хотелось бы показать как сразу вернуться на первый экран, миновав многоуровневый переход выполненный до этого.

На первом View необходимо объявить свойство (в нашем случае это "activateRootLink") при помощи которого будет произведеён переход на следующий View и которое на последующих View будет отслеживаться при помощи @Binding. А уже попав, к примеру, на последний экран в цепочке, мы активируем переход на начальный экран, поменяв значение данного свойства, тем самым схлопнув всю пройденную ранее цепочку экранов.

// первый экран
struct View4: View {
    
    @State private var activateRootLink = false
    
    var body: some View {
        NavigationView {
            NavigationLink(isActive: $activateRootLink, destination: {
                View4_1(activateRootLink: $activateRootLink)
            }, label: {
                Text("Переход на View4_1")
            })
            .navigationTitle("View4")
        }
    }
}

// второй экран
struct View4_1: View {
    
    @Binding var activateRootLink: Bool
    
    var body: some View {
        NavigationLink(destination: {
            View4_2(activateRootLink: $activateRootLink)
        }, label: {
            Text("Переход на View4_2")
        })
        .navigationTitle("View4_1")
    }
}

// третий экран
struct View4_2: View {
    
    @Binding var activateRootLink: Bool
    
    var body: some View {
        VStack{
            Text("Это View4_2")
            Button {
              // активируем переход на первый экран
                activateRootLink = false
            } label: {
                Text("На главный экран")
            }
        }
        .navigationTitle("View4_2")
    }
}
GIF с примером

Комментарии (4)


  1. Yoooriii
    20.02.2022 22:48

    Спасибо, познавательно. А можно это как-то визуально сконструировать? Раньше можно было в Interface Builder UI делать, а как с этим сейчас?


    1. swiftuinotes Автор
      20.02.2022 23:06
      +1

      Так как это было ранее (в Interface Builder), протянуть "segue" от одного экрана к другому, увы не получится, но поверьте, немного попрактиковавшись, к описанному методу привыкаешь и уже не хочется возвращаться "назад".


      1. alnite
        21.02.2022 00:11

        А вроде ж в IB есть все инструменты для работы с segue... Хотя, я тоже предпочитаю код.

        Ну и Swift, наверное-таки, хороший язык, но ощущение какой-то "расхлябанности" от него заставляет ещё больше любить многословную строгость Objective-c :)


      1. Mozhaiskiy
        21.02.2022 01:33

        А ещё раньше, до того как придумали сторибоард, рисовали прямо отдельные экраны в своих nib и xib и грузили их по ходу пьесы. Так, во всяком случае, было написано делать в учебниках по ObjectiveC лет 12 назад.