Одно из самых проблемных мест SwiftUI — работа с навигацией. Отсутствие доступа к стеку навигации, невозможность разделить UI-слой и слой навигации, сложность создания диплинков — всё это затрудняло работу многим разработчикам, использующим SwiftUI в своих проектах. В iOS 16 появился совершенно новый API для работы с навигацией. Что же изменилось с его появлением и стало ли лучше? Я — Светлана Гладышева, iOS-разработчик компании Surf. В статье расскажу про навигацию в Swift UI и её основные проблемы. Сравним работу с навигацией до появления iOS 16 и после. Чтобы лучше понимать изменения, давайте посмотрим, как навигация осуществлялась в старых версиях Swift UI. Основным компонентом навигации был NavigationView — контейнер для управления переходами. Для перехода на другой View использовалась NavigationLink. Например, создать кнопку с текстом «Details», ведущую на экран DetailsView, можно было так: Тут в параметре destination передаём то View, куда хотим перейти. Также существовали способы управлять переходами программно. Можно было использовать параметр isActive у NavigationView, в котором передавался Binding на Bool-свойство: В этом примере при нажатии на кнопку переменная isShowingDetailsView устанавливается в true, и происходит переход в DetailsView. В NavigationLink мы при этом передаем EmptyView, поскольку нам не нужно, чтобы NavigationLink тоже была видна на экране. Была возможность управлять и обратным переходом. Если в этом примере переменную isShowingDetailView установить в false, вернёмся на первый View. Переменных типа Bool могло быть недостаточно, поэтому можно было управлять навигацией через теги: Для каждой NavigationLink добавляем свой тег, а также передаем Binding на свойство selection. Если значение в selection совпадает с тегом у NavigationLink, происходит переход. Если мы после этого установим selection в nil, то вернёмся назад на исходный экран. Selection не обязательно должен был быть типа String, это мог быть любой Hashable-тип. Возможности навигации были ограничены. Навигация была сильно связана с UI-слоем: отделить их друг от друга было практически невозможно. На всех экранах, на которых требовалось сделать переходы, нужно было добавить нужные NavigationLink. Во всех NavigationLink —  обязательно прописать destination, при этом поменять их программно не могли. Из-за этого невозможно было полностью вынести навигацию, например, в роутер. Не получалось легко переходить на несколько уровней вперёд или назад: не было прямого доступа к стеку навигации. Да, мы могли пользоваться параметрами isActive и selection у NavigationLink и управлять их изменениями программно. Например, для простого перехода на root-экран нужно было создать Binding-свойство isActive, привязать его к NavigationLink в первом экране, передать её в нужный View и в нужный момент выставить isActive в false.  Но если надо было сделать много экранов и много переходов с экрана на экран, количество таких Binding-переменных бы сильно разрослось: управлять ими стало очень сложно. Сложность с созданием диплинков. Возьмем для примера список продуктов: в нём по клику на продукт нужно перейти на экран продукта. Предположим, что после обработки диплинка нужно перейти в детали последнего элемента списка. Можно сделать это так: Но если мы запустим этот код для большого количества продуктов, то увидим, что ничего не происходит. Нужный переход произойдет только тогда, когда проскроллим список до конца. Но это не то поведение, которое было нужно. Это несколько наиболее значимых проблем, но список далеко не полный. Для всех этих проблем существовало и другое решение: SwiftUI можно связать со старым добрым UIKit. Поэтому для навигации часто использовали UINavigationController из UIKit. Но всё-таки хотелось бы иметь решение, не зависящее от UIKit. NavigationView стал deprecated: вместо него теперь нужно использовать NavigationStack, который позволяет помещать View в стек навигации и убирать из него. В NavigationLink добавились несколько новых конструкторов: теперь можно передавать параметр value вместо параметра destination. Value — это некий идентификатор того, куда будем переходить. На Value есть ограничение — его тип должен быть Hashable. Чтобы определить, куда же переходить, используется модификатор navigationDestination. Его нужно указать для всех value, которые хотим использовать. Пример: Старые init-методы NavigationLink, в которых есть параметр destination, стали deprecated. Ещё больше возможностей даёт использование NavigationPath: в конструкторе у NavigationStack можно указать параметр path. Можно передать туда непосредственно NavigationPath, а можно коллекцию, хранящую значения типа Hashable. Например, это может быть массив продуктов: При добавлении в path нового элемента будет происходить переход на тот View, который мы прописали в navigationDestination. При удалении N элементов из path будет происходить переход назад на N шагов. В итоге можно полностью управлять навигацией программно, без использования NavigationLink. Появилась возможность легкого управления стеком навигации. Стало очень легко перейти на несколько экранов вперёд или назад. Можно легко вернуться в корневой view. Создание диплинков теперь тоже не представляет особой сложности. Навигация теперь легко выносится из UI-слоя в роутеры или координаторы. Да, нам всё равно придётся во View прописать navigationDestination для всех возможных элементов из NavigationPath, а также указать все View для переходов, но дальше всей навигацией можно управлять извне. Предположим, нужно написать iOS-приложение для интернет-магазина. В таком приложении обычно есть список товаров, корзина, в которую можно добавить товары, выбор адреса и подтверждения заказа. Для простоты оставим только корзину с товарами, выбор адреса и подтверждение заказа. Из корзины можем перейти на экран товара и на экран выбора адреса. С экрана выбора адреса мы должны переходить назад либо на экран подтверждения заказа. С экрана подтверждения заказа мы должны вернуться обратно в корзину. Давайте посмотрим, как можно реализовать переходы между экранами для такого приложения. Полный код приложения можно посмотреть в моём репозитории. Создадим enum Route, в котором будут перечислены все необходимые переходы: Этот enum должен быть Hashable, поскольку он будет использоваться в NavigationPath. Далее создадим класс Router, в котором пропишем все возможные изменения path: Далее прямо в App можем добавить NavigationStack и прописать navigationDestination: Тут мы связываем View и Router, используя path из Router в качестве параметра для NavigationStack. Также в этом View прописан navigationDestination для Route и указано, на какие именно View будут происходить переходы для всех возможных Route. Теперь из ViewModel можем вызывать методы роутера: А View для экрана корзины теперь может выглядеть так: Как видите, здесь нет ни NavigationLink, ни navigationDestination. Мы просто вызываем методы ViewModel. Таким образом, навигацией в этом приложении полностью управляет Router.  Если понадобится добавлять диплинки, сделать это будет очень просто — с помощью методов роутера. Например, если нужно добавить диплинк на продукт, подойдёт метод router.showProduct(). Если хотим использовать координаторы, всё становится немного сложнее. В navigationDestination мы должны указывать View. Поэтому, если наш координатор будет сам создавать модули экранов, нужно будет в navigationDestination получать View из координатора. Кроме того, NavigationStack не предоставляет возможность изменить корневой View, поэтому подменить его из координатора не получится. К сожалению, использовать новый API в iOS 15 и в более ранних версиях не получится. В остальном — работать с навигацией в SwiftUI теперь действительно стало легче и удобней. Хочется, чтобы SwiftUI и дальше развивался и становился все лучше. Чтобы глубже погрузиться в тему, рекомендую видео The SwiftUI cookbook for navigation с WWDC 2022. В нём помимо работы с NavigationStack и NavigationPath наглядно показывается работа с NavigationSplitView, а также сохранение и восстановление состояния навигации. Больше полезного про iOS — в нашем телеграм-канале Surf iOS Team. Публикуем кейсы, лучшие практики, новости и вакансии Surf. Присоединяйтесь >> Больше кейсов команды Surf ищите на сайте >>