Как стать автором
Обновить

iOS: Навигация по-новому

Время на прочтение3 мин
Количество просмотров6.1K

С каждый днем все больше разработчиков 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 брекпоинтов.

Код доступен на GitHub

Обсудить можно на телеграмм канале.

Предыдущая статья о кастомной навигации доступна на хабре.

Теги:
Хабы:
Всего голосов 7: ↑7 и ↓0+7
Комментарии5

Публикации

Истории

Работа

Swift разработчик
16 вакансий
iOS разработчик
16 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань