Навигация кажется простой, пока не появляются deep links, вход по ссылке, восстановление после выгрузки и экраны, которые нужны в нескольких потоках. Здесь общие для всех платформ принципы.
Почему не внутри экранов
Если экран A сам создаёт экран B и делает push, экраны связаны: A нельзя переиспользовать в другом потоке, deep link должен знать цепочку экранов, а тестировать переходы неудобно. Решение: экраны только сообщают о событиях («выбран товар 42»), а куда перейти, решает отдельный объект.
Coordinator (iOS, UIKit)
protocol Coordinator: AnyObject { func start() }
final class ShopCoordinator: Coordinator {
private let nav: UINavigationController
private var children: [Coordinator] = []
func start() {
let list = ProductListViewController()
list.onSelect = { [weak self] id in self?.showProduct(id) }
nav.pushViewController(list, animated: false)
}
func showProduct(_ id: Int) { ... }
}
Координаторы образуют дерево по потокам: приложение, авторизация, основной поток, оформление заказа. Дочерний сообщает родителю о завершении, и тот его удаляет. Ловушка: системный «назад» и закрытие модального окна свайпом не вызывают код координатора, их отслеживают через делегаты, иначе дочерние координаторы утекают.
Навигация как состояние
В декларативных UI стек экранов описывают данными:
- SwiftUI:
NavigationStack(path:)иnavigationDestination(for:); - Compose: Navigation Compose с типобезопасными маршрутами;
- Flutter: go_router или Navigator 2.0;
- React Native: React Navigation.
Плюсы: deep link превращается в «построй массив маршрутов», восстановление после выгрузки в «сохрани массив», тест в «проверь массив после действия».
На Android важно управлять back stack: после входа или онбординга экраны до них удаляют (popUpTo(...) { inclusive = true }), чтобы «назад» не возвращал на логин.
Модально или push
Push продолжает иерархию: список → детали. Модальный экран это отдельная задача со своим началом и концом: написать сообщение, оплатить, выбрать фильтры.
Deep links
- Своя схема
myapp://product/42: любое приложение может её зарегистрировать, без приложения ссылка не откроется. - Universal Links (iOS) и App Links (Android): обычные https-ссылки, связь подтверждена файлом на домене (
apple-app-site-association,assetlinks.json). Без приложения откроется сайт, перехватить ссылку чужое приложение не может. - Отложенные deep links: пользователь пришёл по ссылке, приложение ещё не установлено; после установки он должен попасть на нужный экран. Делают через сервисы атрибуции или Install Referrer.
Обработка:
- Система передаёт URL.
- Роутер разбирает его в типизированный маршрут и валидирует параметры.
- Проверяется готовность приложения: вход, онбординг, конфигурация.
- Если нужен вход, маршрут сохраняется как отложенный и выполняется после.
- Строится стек экранов до цели, а не одиночный экран.
Push-уведомления и виджеты используют тот же роутер: один код для всех внешних входов.
Безопасность ссылок
Deep link может сформировать кто угодно. Нельзя по ссылке сразу выполнять действия (перевод, подписку, удаление) без подтверждения, открывать служебные экраны или доверять параметрам. На Android не экспортируют лишние Activity.
Восстановление после выгрузки
Система выгружает приложение в фоне постоянно. На Android back stack восстанавливается системой, но данные из памяти теряются: состояние экранов сохраняют в SavedStateHandle и rememberSaveable. На iOS сохраняют путь навигации и черновики сами.
Что спросят на собесе
- Зачем нужен Coordinator и как избежать утечек дочерних координаторов?
- Чем Universal Links лучше собственной схемы URL?
- Как открыть по ссылке экран, требующий входа?
- Как устроена навигация в SwiftUI, Compose или Flutter?
- Как восстановить экран после выгрузки процесса?