Connect with us

Разработка

Распутываем навигацию SwiftUI

Модель навигации SwiftUI поначалу может показаться немного запутанной, но как только вы поймете правила игры, она окажется одновременно элегантной и мощной.

Опубликовано

/

     
     

Это руководство раскрывает тайны навигационной системы SwiftUI — от настройки TabView до глубинных ссылок и расширенных NavigationPaths.

Введение: почему навигация до сих пор всех сбивает с толку (включая меня)

Если бы SwiftUI был парком развлечений, навигация была бы домом с привидениями — сначала захватывающе, но быстро понимаешь, что не знаешь, как из него выбраться.

Apple подарила нам NavigationView, а затем деликатно похлопала по плечу, выпустив новенький блестящий NavigationStack. Добавьте NavigationPath, onOpenURL и всеми любимый загадочный ящик — диплинкинг — и теперь даже опытные iOS-разработчики выглядят как тот мем с парнем, вокруг которого парят математические уравнения.

Но не волнуйтесь. Создаёте ли вы простое приложение с несколькими экранами или проектируете динамический поток с URL-адресами и кастомными типами данных, SwiftUI действительно может сделать простую навигацию. Как только вы разберётесь с инструментами (и будете знать, когда с ними не стоит бороться), всё станет на свои места.

Вот что вы узнаете из этой статьи:

  • Как использовать TabView для быстрой навигации на нескольких экранах
  • Как NavigationStack заменяет устаревший NavigationView
  • Что, чёрт возьми, делает NavigationPath и зачем он нужен
  • Как работать с глубокими ссылками и заставить приложение реагировать на URL-адреса
  • Распространённые ошибки и как не нарушить логику навигации

Давайте начнём с самого простого инструмента навигации SwiftUI: TabView

TabView: самый простой инструмент навигации SwiftUI

Если NavigationStack — это новая модная игрушка, то TabView — это старый добрый велосипед, к которому вы всегда возвращаетесь — простой, надёжный и на удивление увлекательный.

Что такое TabView? Это способ SwiftUI, позволяющий пользователям переключаться между различными разделами вашего приложения с помощью нижней панели вкладок — представьте себе домашнюю страницу, поиск, рилсы, профиль… ну, вы поняли.

Вот как легко начать:

struct ContentView: View {
    var body: some View {
        TabView {
            HomeView()
                .tabItem {
                    Label("Home", systemImage: "house.fill")
                }

            SettingsView()
                .tabItem {
                    Label("Settings", systemImage: "gear")
                }
        }
    }
}

Два представления, две вкладки и работающее приложение. Никаких роутеров, никакой магии.

Советы профессионалов:

  • Используйте Label(title:image:) для красивого сочетания значка и текста.
  • SF Symbols создают ощущение нативных элементов.
  • SwiftUI сохраняет состояние каждой вкладки. Так что, если у вашей вкладки есть счётчик @State, и вы переключаетесь на другую вкладку — он запомнится! Удобно или раздражает — в зависимости от вашего дизайна.
  • Хотите скрыть панель вкладок? Оберните TabView в кастомное условие, но не пытайтесь принудительно скрывать его для каждого экрана — SwiftUI может дать сбой.

Распространённая путаница: TabView + NavigationStack

Да, вы можете объединить оба элемента — просто поместите NavigationStack внутрь каждой вкладки, если хотите, чтобы экраны в них располагались. Вот так:

TabView {
    NavigationStack {
        HomeView()
    }
    .tabItem { Label("Home", systemImage: "house") }

    NavigationStack {
        SettingsView()
    }
    .tabItem { Label("Settings", systemImage: "gear") }
}

И вот так у каждой вкладки есть свой собственный контекст навигации.

NavigationStack: попрощайтесь с NavigationView

Помните NavigationView? Что ж, Apple тихонько отказалась от него, начиная с iOS 16, и приветствовала NavigationStack как новый инструмент для навигации между экранами. И нет, это не просто переименование — это совершенно новый подход.

Что такое NavigationStack?

Представьте его как современную, более мощную версию NavigationView, созданную для лучшей работы с развивающейся природой SwiftUI, основанной на данных. Он позволяет вам добавлять и выводить представления, используя типы значений, с большим контролем над историей навигации.

Вот простой пример:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Go to Detail", value: "SwiftUI ❤️ NavigationStack")
            }
            .navigationDestination(for: String.self) { value in
                Text("You selected: \(value)")
            }
            .navigationTitle("Home")
        }
    }
}

Ключевые концепции:

  • NavigationStack заменяет NavigationView, начиная с iOS 16.
  • Используйте NavigationLink(value:) с navigationDestination(for:) для создания типобезопасной динамической навигации.
  • Вы определяете, какой тип данных передается и как он должен рендерится при пуше.

Когда использовать:

  • Когда нужен глубокий контроль над обработкой навигации.
  • Когда передаете данные (например, модель или строку) на следующий экран.
  • Когда нужен предсказуемый и декларативный способ пуша представлений — без странных ошибок, которыми так славился NavigationView.

Небольшой сдвиг, большое влияние

Важно отметить: с NavigationStack вы больше не привязываете навигацию к иерархиям представлений. Вместо этого это больше похоже на создание стека значений данных — и SwiftUI сам определяет, как их отобразить.

Как NavigationLink и .navigationDestination работают вместе

В NavigationStack навигация управляется значениями, а не жёстко заданными представлениями назначения. Это сильно отличается от того, как раньше работал NavigationView. Давайте разберёмся:

Шаг 1: Используйте NavigationLink(value:) для передачи значения

Вместо того, чтобы писать что-то вроде NavigationLink(destination: DetailView()), теперь вы передаете значение определённого типа:

NavigationLink("Go to Profile", value: "karanpal")

Эта строка говорит:

При нажатии поместить представление в стек навигации и использовать строковое значение «karanpal», чтобы определить, какое представление отображать.

  • Go to Profile → Это метка, которая отображается в пользовательском интерфейсе. Это то, что видит пользователь и нажимает.
  • karanpal → Это значение, которое помещается в NavigationStack при тапе.

Шаг 2: Определите, что делать с этим значением, с помощью .navigationDestination(for:)

Здесь вы объявляете, как следует обрабатывать конкретный тип при его помещении в стек:

.navigationDestination(for: String.self) { username in
    ProfileView(username: username)
}

Это означает:

Если передана строка, использовать её для построения данного представления.

Простыми словами

  • NavigationLink(value:) добавляет значение в NavigationStack.
  • .navigationDestination(for:) сообщает SwiftUI, как преобразовать это значение в представление.
  • Затем SwiftUI автоматически сопоставляет тип переданного значения с замыканием .navigationDestination и отображает соответствующее представление.

NavigationPath: для случаев, когда вам нужен полный контроль

К настоящему моменту NavigationLink(value:) и .navigationDestination(for:) кажутся отличным дуэтом. Но что, если навигация вашего приложения станет сложнее?

Возможно, вы:

  • Перемещаетесь между разными типами пунктов назначения
  • Создаёте собственный стек переходов (да, такое возможно!)
  • Восстанавливаете состояние навигации из сохранённых данных или глубокой ссылки

Вот тут-то и вступает в дело NavigationPath.

Что такое NavigationPath?

NavigationPath — это специальный объект SwiftUI, который действует как type-erased стек значений. Он позволяет программно добавлять, извлекать и манипулировать стеком — без участия пользователя.

Представьте это так: «Я буду отслеживать всё, куда был выполнен переход, и мне всё равно, какой это тип — просто скажите мне, как это отобразить».

Простой пример использования

Допустим, мы хотим пушить разные типы данных — данные о пользователе или статью.

struct User: Hashable { let name: String }
struct Article: Hashable { let title: String }

Вот как настроить динамический путь навигации:

@State private var path = NavigationPath()

NavigationStack(path: $path) {
    List {
        Button("Open User") {
            path.append(User(name: "Karan"))
        }
        Button("Open Article") {
            path.append(Article(title: "SwiftUI Tips"))
        }
    }
    .navigationDestination(for: User.self) { user in
        Text("User: \(user.name)")
    }
    .navigationDestination(for: Article.self) { article in
        Text("Article: \(article.title)")
    }
}

Зачем это нужно?

С NavigationPath вы можете:

  • Перемещаться программно — пушить данные из логики, а не только по нажатиям
  • Обрабатывать несколько типов в одном стеке
  • Делать Pop или сбрасывать стек (path.removeLast(), path.removeAll())
  • Сохранять/восстанавливать историю навигации из состояния (отлично подходит для диплинкинга или сохранения сессий)

Реальные сценарии

  • Глубокие ссылки: можно вручную построить путь, соответствующий URL-адресу, например, myapp://user/Karan/article/SwiftUI
  • Восстановление состояния: когда пользователь снова открывает приложение, можно перестроить путь, чтобы вернуть его на место остановки
  • Потоки онбординга: программно запушить несколько представлений подряд при выполнении условия

NavigationPath — это как предоставить SwiftUI динамический GPS, а не просто следовать статическому маршруту. Вы управляете стеком. Вы пушите значения. А SwiftUI выполняет рендеринг.

Что дальше? Давайте изучим диплинкинг и наконец-то заставим URL-адреса myapp:// работать так, как им положено.

Глубокие ссылки в SwiftUI: не так страшно, как кажется

Вы когда-нибудь нажимали на уведомление или ссылку на веб-сайте и попадали в глубины приложения, минуя заставку, словно VIP-гость? Это и есть диплинкинг. И да — SwiftUI поддерживает его нативно, без магии UIKit.

Что такое глубокие ссылки

Глубокие ссылки — это то, как ваше приложение открывает определённый экран при нажатии на URL-адрес, например:

myapp://profile/karanpal
myapp://article/swiftui-navigation

Они очень полезны для:

  • Открытия приложения из Safari или Почты
  • Нажатия на кнопки push-уведомлений
  • Внутренних переходов между разделами

Обработка глубоких ссылок в SwiftUI: AppDelegate не нужен

В UIKit всё это обрабатывалось через AppDelegate. В SwiftUI это обрабатывается с помощью .onOpenURL {} — и вот ключевая часть:

.onOpenURL следует разместить на верхнем уровне вашего приложения, в идеале внутри структуры App, чтобы гарантировать перехват ссылок независимо от того, где находится пользователь.

Пример: обработка ссылки на профиль

Допустим, вы хотите сделать ссылку на профиль:

myapp://profile/karanpal

Вот как это сделать в структуре App вашего SwiftUI-приложения:

@main
struct MyApp: App {
    @State private var path = NavigationPath()

    var body: some Scene {
        WindowGroup {
            NavigationStack(path: $path) {
                HomeView()
                    .onOpenURL { url in
                        handleDeepLink(url)
                    }
                    .navigationDestination(for: User.self) { user in
                        ProfileView(username: user.name)
                    }
            }
        }
    }

    private func handleDeepLink(_ url: URL) {
        guard let host = url.host else { return }

        switch host {
        case "profile":
            if let username = url.pathComponents.dropFirst().first {
                path.append(User(name: username))
            }
        default:
            break
        }
    }
}

Такая настройка гарантирует:

  • Ваше приложение прослушивает диплинки сразу после запуска.
  • Вам не нужно зависеть от активности какого-то случайного экрана для перехвата URL.
  • Вы можете пушить data-driven назначения с помощью NavigationPath.

Регистрация вашей URL-схемы

Чтобы сообщить iOS, что ваше приложение обрабатывает такие типы ссылок:

  1. Перейдите в настройки .xcodeproj вашего приложения
  2. Target → Info → URL Types
  3. Добавьте новую запись с вашей пользовательской схемой (myapp)

Вуаля — теперь ваше приложение поддерживает глубокие ссылки.

Советы и подсказки: что сэкономит вам часы отладки

Навигационная система SwiftUI элегантна, современна и декларативна… пока внезапно не перестаёт быть таковой. Вот несколько реальных подвохов (и как их обходить, как настоящий ниндзя Swift).

1. NavigationLink не срабатывает? Проверьте тип значения

Если нажатие на NavigationLink(value:) ничего не происходит, обычно это из-за следующих причин:

  • Вы не определили .navigationDestination(for:) для нужного типа
  • Или вы передали не хешируемое/optional значение

Исправление: всегда проверяйте, совпадают ли тип значения и тип .navigationDestination(for:) — не допускается Optional, не допускаются несоответствия.

2. Вложение NavigationStack в другой? SwiftUI может запутаться

Пытаетесь вложить NavigationStack в другой (например, во вкладку)? Технически допустимо. Логически опасно.

Вы можете увидеть:

  • Странное поведение навигации
  • .navigationDestination не срабатывает
  • Кнопка «Назад» исчезает

Исправление: избегайте вложенных стеков. Вместо этого создавайте новый NavigationStack на уровне TabView — каждая вкладка должна иметь свой собственный стек.

3. Изменение path вручную? Легко разрушить стек

Хотя path.append(...) — мощный метод, слепое добавление значений может:

  • Нарушить иерархию представления, если .navigationDestination не определен
  • Заставить SwiftUI молча игнорировать навигацию

Исправление: проверьте правильность перед добавлением. Кроме того, если вы восстанавливаете состояние из JSON или глубокой ссылки, перестройте путь в правильном порядке.

4. Состояние навигации не сбрасывается при выходе из системы

Если вы используете NavigationPath, и пользователь выходит из системы, он может всё ещё оставаться в стеке при повторном входе.

Исправление: при выходе из системы вызовите:

path.removeAll()

Это сбрасывает стек, возвращая пользователя в начало навигационного потока.

5. TabView + NavigationStack: остерегайтесь путаницы состояний

У каждой вкладки может быть свой NavigationStack, но если вы используете общее состояние для вкладок, SwiftUI может вызвать непредвиденное поведение (например, возврат к глубокому экрану после переключения вкладок).

Исправление: используйте отдельные модели представления для каждой вкладки или сбрасывайте состояние навигации при переключении вкладок пользователем, если это необходимо.

Краткое содержание: вы только что распутали навигационную сеть SwiftUI

Если вы дошли до этого места — поздравляем!  Теперь вы понимаете навигацию SwiftUI больше, чем большинство разработчиков, которые в ярости бросают на полпути создания панели вкладок.

Вот краткий обзор того, что мы рассмотрели:

  • TabView: самый простой способ переключаться между разделами приложения с минимальным кодом и максимальной функциональностью.
  • NavigationStack: современная замена NavigationView, использующая навигацию, основанную на значениях.
  • NavigationLink + .navigationDestination: декларативный дуэт — представления запускаются значениями, а не императивными пушами представлений.
  • NavigationPath: когда вы хотите стать профессионалом. Программная навигация, динамические стеки и глубокий контроль.
  • Глубокие ссылки: используйте .onOpenURL в точке входа в приложение, анализируйте URL-адреса и отправляйте значения в стек.
  • Подводные камни: от странного поведения при возврате данных до сброса состояния при входе в систему — теперь вы знаете, чего следует избегать и как это исправить.

Модель навигации SwiftUI поначалу может показаться немного запутанной, но как только вы поймете правила игры, она окажется одновременно элегантной и мощной.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: