2 решения для маршрутизации в SwiftUI

Цели

  • Отделите логику навигации от слоя представления. Это позволит вам изменять навигацию без изменения представлений.
  • Упростите реализацию таких задач, как создание прямых ссылок, сделав навигацию более гибкой.

Информация о SwiftUI

Декларативный характер SwiftUI затрудняет отделение навигации от уровня представления. Для push-навигации необходимо вставить NavigationLink в представление. А модальное представление требует добавления модификатора .sheet где-нибудь в коде представления. Вы должны указать конечный вид как для NavigationLink, так и для .sheet. Кроме того, как правило, для запуска навигации используется свойство @state.

Это отличается от UIKit. Например, чтобы представить представление в UIKit модально, достаточно вызвать present на текущем контроллере представления и передать целевой контроллер представления для отображения. Это можно легко сделать вне контроллера представления представления.

Решения

Мы рассмотрим два несколько разных решения, чтобы отделить навигацию от слоя представления. Оба используют собственные методы навигации SwiftUI NavigationLink и .sheet модификатор. Но предоставление вида назначения и состояния навигации будет перенесено на маршрутизатор.

  1. Маршрутизатор с триггерными представлениями. Маршрутизатор будет возвращать триггерные подвиды для всех возможных маршрутов навигации, чтобы вставить их в презентационное представление. Такой фрагмент кода подпредставления будет содержать внутри модификатор NavigationLink или .sheet, а также указанное целевое представление и будет использовать свойство @state, хранящееся в маршрутизаторе, через привязку. Таким образом, представление не будет зависеть от кода навигации и пункта назначения, только от протокола маршрутизатора.
  2. Маршрутизатор с модификаторами со стиранием типа. Представление представления будет настроено с общими модификаторами для представления любых других представлений. Будучи инициализированы маршрутизатором, эти модификаторы будут отслеживать состояние навигации, хранящееся в маршрутизаторе, через привязки и выполнять навигацию, когда маршрутизатор меняет это состояние. Роутер также будет иметь функции для всевозможной навигации. В результате эти функции изменят состояние и запустят навигацию.

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

Чтобы облегчить понимание, давайте рассмотрим оба решения в контексте простого примера.

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

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: