2 решения для маршрутизации в SwiftUI
Цели
- Отделите логику навигации от слоя представления. Это позволит вам изменять навигацию без изменения представлений.
- Упростите реализацию таких задач, как создание прямых ссылок, сделав навигацию более гибкой.
Информация о SwiftUI
Декларативный характер SwiftUI затрудняет отделение навигации от уровня представления. Для push-навигации необходимо вставить NavigationLink
в представление. А модальное представление требует добавления модификатора .sheet
где-нибудь в коде представления. Вы должны указать конечный вид как для NavigationLink
, так и для .sheet
. Кроме того, как правило, для запуска навигации используется свойство @state
.
Это отличается от UIKit. Например, чтобы представить представление в UIKit модально, достаточно вызвать present
на текущем контроллере представления и передать целевой контроллер представления для отображения. Это можно легко сделать вне контроллера представления представления.
Решения
Мы рассмотрим два несколько разных решения, чтобы отделить навигацию от слоя представления. Оба используют собственные методы навигации SwiftUI NavigationLink
и .sheet
модификатор. Но предоставление вида назначения и состояния навигации будет перенесено на маршрутизатор.
- Маршрутизатор с триггерными представлениями. Маршрутизатор будет возвращать триггерные подвиды для всех возможных маршрутов навигации, чтобы вставить их в презентационное представление. Такой фрагмент кода подпредставления будет содержать внутри модификатор
NavigationLink
или.sheet
, а также указанное целевое представление и будет использовать свойство@state
, хранящееся в маршрутизаторе, через привязку. Таким образом, представление не будет зависеть от кода навигации и пункта назначения, только от протокола маршрутизатора. - Маршрутизатор с модификаторами со стиранием типа. Представление представления будет настроено с общими модификаторами для представления любых других представлений. Будучи инициализированы маршрутизатором, эти модификаторы будут отслеживать состояние навигации, хранящееся в маршрутизаторе, через привязки и выполнять навигацию, когда маршрутизатор меняет это состояние. Роутер также будет иметь функции для всевозможной навигации. В результате эти функции изменят состояние и запустят навигацию.
Пример использования
Чтобы облегчить понимание, давайте рассмотрим оба решения в контексте простого примера.
Проект будет состоять из двух простых представлений: представления с одной кнопкой и представленного представления, к которому следует переходить, когда пользователь нажимает кнопку. Представленное представление будет отображать текст, переданный из представления представления. Нажатие на этот текст закроет представленное представление.
1. Маршрутизатор с триггерами
Представляя вид
Представляющее представление инициализируется маршрутизатором, который знает, как перейти к представленному представлению. Маршрутизатор обеспечивает представление триггера, которое после вставки может выполнять желаемую навигацию в ответ на действия пользователя.
Использование PresentingRouterProtocol
вместо фактического класса маршрутизатора позволяет нам легко изменять логику навигации и инициализировать PresentingView
с помощью фиктивного маршрутизатора для тестов.
Мы должны заключить свойство маршрутизатора в @StateObject
(доступно в iOS 14). Это связано с тем, что состояние навигации находится в маршрутизаторе, и представление должно обновляться при изменении состояния. Кроме того, в отличие от @ObservedObject
, эта оболочка избегает воссоздания маршрутизатора при каждом обновлении представления владельца. Это очень важно для нашей реализации push-навигации на основе NavigationLink
.
Протоколы маршрутизатора
Есть два протокола. Первый используется маршрутизаторами, которые могут переходить к другим представлениям.
NavigationState
будет реализован как структура в подклассах. Он будет содержать переменные состояния для всех возможных маршрутов навигации. binding
- удобная функция , используемая для получения привязок для существующих переменных состояния.
Другой протокол используется для маршрутизаторов представлений, которые были представлены модально или помещены в представление навигации. Это позволяет нам вернуться назад.
Обычно маршрутизаторы соответствуют протоколу PresentingRouter
и PresentedRouter
.
Реализация роутера
Маршрутизатор, соответствующий PresentingRouterProtocol
, должен иметь возможность переходить к просмотру сведений. Таким образом, его navigationState
должен содержать переменную для этого маршрута.
Функция presentDetails
использует функцию binding
из расширения протокола NavigatingRouter
для обеспечения доступа к соответствующей переменной состояния как к маршрутизатору представления назначения, так и к представлению возвращающего триггера. Маршрутизатор назначения будет использовать эту привязку для отклонения. И представление триггера будет использовать привязку для запуска навигации.
Просмотры триггера
Маршрутизатор возвращает триггерные представления, которые выполняют фактическую навигацию в ответ на действия пользователя. Мы рассмотрим только два наиболее полезных типа триггерных просмотров, хотя их может быть больше. Оба берут представление содержимого и преобразуют его в результирующую кнопку, с помощью которой можно переходить к указанному целевому представлению.
Первый - SheetButton
,, используемый в приведенном выше примере. Он реализует модальную навигацию с помощью модификатора SwiftUI .sheet
.
Второй - NavigationButton
. Он обеспечивает push-навигацию с NavigationLink
внутри.
Мы используем инициализатор NavigationLink
с isActive
здесь, так как мы хотим сохранить состояние навигации в маршрутизаторе.
Наличие одинаковых параметров инициализации в SheetButton
и NavigationButton
позволяет легко изменить тип навигации с модального представления на навигацию по нажатию - и наоборот.
BasePresentRouter класс
Отказ от функциональности очень распространен. Итак, мы также представим простой базовый класс маршрутизатора.
Представленный вид
Просмотр сведений может быть только представлен и не выполняет никакой другой навигации. По этой причине мы будем использовать базовый BasePresentedRouter
маршрутизатор без подклассов.
Мы можем объявить свойство router
без оболочки в PresentedView
, потому что мы используем его только для отклонения, поэтому представление не нужно прослушивать изменения состояния маршрутизатора.
Мы скрываем представление, когда пользователь нажимает на его текст.
2. Маршрутизатор с модификаторами со стиранием типа
Навигация в этом решении реализована иначе - с помощью модификаторов. Я называю их стираемыми по типу, потому что они не зависят от конкретных просмотров места назначения. Будучи добавленными один раз, они могут перейти к любому виду.
Представляя вид
Чтобы разрешить перенос любого представления в представление навигации, мы должны добавить модификатор .navigation
и передать маршрутизатор как единственный параметр. Точно так же, чтобы включить представление листа, мы используем модификатор .sheet
. Мы можем добавить его в любое подпредставление внутри body
. Это настраиваемый модификатор .sheet
, который принимает в качестве параметра маршрутизатор.
Представление переходит в представленное представление путем вызова функции маршрутизатора presentDetails
из действия кнопки.
Реализация роутера
Базовый класс Router
реализует протокол ObservableObject
, и его состояние заключено в @Published
, чтобы позволить изменениям состояния распространяться на представление и обновлять навигацию.
Переменная navigating
запускает push-навигацию на NavigationView
. Его значением должно быть целевое представление.
presentingSheet
управляет представлением листов. Его значение также должно быть видом назначения.
isPresented
- это привязка типа bool к переменной состояния, которая управляет переходом к текущему представлению. Он должен быть передан от представляющего маршрутизатора.
Push или модальная навигация может быть запущена путем установки целевого представления для свойства состояния navigation
или presentingSheet
соответственно. Ниже представлен удобный интерфейс для подклассов роутера.
Как видно выше, маршрутизатор должен быть инициализирован с привязкой к свойству Bool
, которое управляет представлением представления владельца. Мы можем получить его от представляющего маршрутизатора со следующим интерфейсом:
boolBinding
это частная функция, реализованная с доступом к состоянию по ключу. Ниже показано частное расширение маршрутизатора, которое также содержит функцию binding
, используемую для модификаторов .navigation
и .sheet
.
Модификаторы .navigation и .sheet
.navigation
и .sheet
на самом деле являются функциями, добавленными к расширению View
. Они принимают маршрутизатор в качестве параметра для связывания результирующих модификаторов представления с соответствующим состоянием навигации в маршрутизаторе: navigation
и presentingSheet
переменные.
NavigationModifier
реализован с NavigationLink
внутри и использует предоставленную привязку для запуска презентации.
SheetModifier
реализован с модификатором SwiftUI .sheet
очень похожим образом.
Создание подкласса маршрутизатора
Вышеупомянутая реализация базового класса Router
позволяет его подклассам быть очень простыми. Нам нужно только добавить функции для доступных вариантов навигации.
В нашем примере это будет одна presentDetails
функция. Чтобы выполнить навигацию, нам нужно создать представление назначения, инициализировать его соответствующим маршрутизатором и использовать один из удобных методов базового класса (navigateTo
или presentSheet
) для запуска желаемого типа навигации.
Затем мы можем вызвать его из любого кода точки зрения, когда мы хотим представить PresentedView
. В нашем примере это закрытие действия кнопки.
Если мы хотим переключиться с push-навигации на модальную, единственное, что нам нужно сделать, это заменить вызов navigateTo
на presentSheet
.
Представленный вид
То же, что и в первом решении.
Глубокие ссылки
Оба решения имеют состояние навигации внутри маршрутизаторов. Это позволяет нам выполнять навигацию и внедрять глубокие ссылки, просто изменяя состояние маршрутизатора. Первое решение, однако, требует добавления надлежащих представлений триггеров, чтобы разрешить возможные маршруты глубинных ссылок. Второе решение использует стирание типа для целевых представлений. Так что нужны только общие модификаторы .navigation
и .sheet
.
Сравнение
Оба решения отделяют логику навигации от уровня представления. Оба хранят состояние навигации в маршрутизаторе. Оба нуждаются в некотором коде, который нужно вставить в представление для навигации. Этот код использует внутри модификатор NavigationLink
или .sheet
.
Изменение типа навигации с модального на push-навигацию и наоборот тривиально в обоих решениях.
В первом решении навигация выполняется с помощью представлений триггеров, возвращаемых маршрутизатором. И во втором решении используются универсальные модификаторы со стиранием типа. В первом решении нам нужно предварительно настроить представление со всеми возможными маршрутами, вставив соответствующие подвиды триггеров. Во втором решении добавление нового маршрута не требует изменения представления, поскольку модификаторы .navigation
и .sheet
поддерживают любое представление пункта назначения.
Например, предположим, что некоторое представление может переходить к трем различным представлениям, помещая их в стек навигации. В этом случае нам нужно будет вставить три представления триггеров в первое решение и только один модификатор .navigation
во второе решение. Это различие делает навигацию более гибкой во втором решении.
Во втором решении создание подклассов проще, поскольку существует базовый суперкласс маршрутизатора, в котором хранится все состояние навигации.
Недостатком второго решения является то, что базовый класс должен хранить все три переменные состояния навигации: navigating
, presentingSheet
и isPresented
, хотя не все из них необходимы для использования конкретным подклассом.
Ресурсы
Вы можете найти исходный код, использованный в этой статье, в моих репозиториях на GitHub: