С каждый днем все больше разработчиков 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. Первым бросается в глаза то, что при переходе на более чем один уровень вложенности – слетает анимация. Это происходит если путь заменяется единственным элементом в массиве. При добавлении нового элемента в массив  – такого не происходит.

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

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

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

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


  1. spiceginger
    24.10.2022 14:43

    А можно в ручную возвращаться к предыдущему экрану?


    1. Demtriy Автор
      24.10.2022 14:55

      Конечно.

      Либо откусить последний элемент от массива, либо в путь засеттить конкретный экран:
      Допустим, есть путь.
      path == ["first", "second", "third", "fourth"]

      Либо откусываем "fourth"
      model.path = ["first", "second", "third"]
      Либо сеттим "third"
      model.path = ["third"]


      1. spiceginger
        24.10.2022 14:56

        Спасибо. Просто по контексту не ясно.