С каждый днем все больше разработчиков IOS стремятся свои новые проекты начинать с использованием SwiftUI. И здесь перед ними возникает проблемы в виде реализации устоявшихся представлений о навигации в iOS. Предлагаемые решения от Apple работают весьма часто довольно криво. Это понимают и в самой Apple. По мере развития SwiftUI основной компонент навигации NavigationView был заменен на NavigationStack. И это не просто переименование. Те кто уже использовал NavigationView не готовы от него отказаться, так как его реализация лежала через боль и слезы. Те же кто только входит в мир SUI либо наталкиваются на рекомендации создавать кастомную навигацию, либо смотрят на статьи как разруливать проблемы NavigationView. Новая альтернатива не всем пришлась по-душе, так как на WWDC не продемонстрировали его с лучшей стороны. А она есть. И это хорошая новость! Apple, наконец, освоила паттерн Navigator, которым конкуренты пользовались более 10 лет!
В чем суть: теперь навигация становится возможной даже при помощи передачи пути для навигации. Те кто пользовался DeepLink или UniversalLink возрадуются. Теперь и на их улице будет праздник.
Hidden text
Это не значит что, раньше это было невозможно, но теперь не приходится для этого устраивать танцы с бубном.
Чтоб продемонстрировать идею, был набросан минимальный проект, включающий пять экранов, с незамысловатыми названиями: first, second, third и fourh. Эти экраны были объединены следующей схемой переходов:
Здесь, сплошной линией со стрелкой обозначен прямой переход на указанный экран. Толстой стрелкой показан переход на четвертый экран по пути навигации. Переходы по нажатию на «Back» и переход на главный экран не обозначены, чтоб не захламлять схему.
Вместо набившим оскомину NavigationLink в приложении был использован обычный Button, так как он значительно лучше поддается кастомизации.
Вся навигация сводится к передаче массива с условными названиями экранов в переменную пути. NavigarionStack пройдет все цепочку навигации автоматически, и покажет последний экран в цепочке.
Так для того, чтоб показать экран «Fourth» с сохранением всей цепи навигации достаточно в переменную передать массив [«first», «second», «third», «fourh»]. Соответственно, и возврат по «Back» будет происходить в обратном порядке.
Button { model.path = ["first", "second", "third", "fourth"] }
label: { ButtonContent("The furthest view") }
Но так как переход на следующий экран происходит через передачу его имени как единственного элемента массива – то возврат будет осуществляться сразу на главный экран, миную предыдущие экраны. Вполне очевидно, чтоб возвращаться по полной цепи – нужно добавлять имя экрана в переменную пути как отдельный элемент массива.
В отличите от UIKit, NavigationStack не хранит в себе состояния предыдущих экранов. Таким образом, при возврате - View и его ViewModel будет воссоздана с нуля – это следует учитывать при создании архитектуры UI, когда необходимо сохранить пользовательские данные, или вернуть состояние Scroll / таблицы к предыдущей ячейке.
Ключевой особенностью реализации всей схемы является метод, который возвращает View по его имени в пути. Понятное дело, что в реальном проекте именование лучше осуществлять через Enum, но для демонстрации это не имеет большого значения. Если сравнить со статьей про кастомную реализацию навигации при помощи координатора – вполне очевидно, что такая навигация значительно проще.
class Coordinator: ObservableObject {
@Published var path: [String] = []
func resolve(pathItem:String) -> some View {
Group {
switch pathItem {
case "first": FirstView()
case "second": SecondView()
case "third": ThirdView()
case "fourth": FourthView()
default:
EmptyView()
}
}
}
}
Для возврата на корневой экран, достаточно массиву пути присвоить пустое значение:
Button { model.path = [] } label: { ButtonContent("Root View") }
Скорее всего, NavigationStack все еще хранит в себе множество неприятных сюрпризов, доставшихся по наследству от NavigationView. Первым бросается в глаза то, что при переходе на более чем один уровень вложенности – слетает анимация. Это происходит если путь заменяется единственным элементом в массиве. При добавлении нового элемента в массив – такого не происходит.
UPDATE:
Если задействовать в приложении второй NavigationStack (что является распространенной практикой в UIKit, особенно для реализации вкладок TabBar), а потом перейти на View которое этот NavigationStack использует, то все будет выглядеть вполне "нормально", пока, в скором времени, приложение не рухнет при выполнении безобидной операции в непредсказуемом месте. В консоль при этом выводится сообщение: "Fatal error: 'try!' expression unexpectedly raised an error: SwiftUI.AnyNavigationPath.Error.comparisonTypeMismatch", которое невозможно перехватить при помощи установки exception брекпоинтов.