Простая, мощная и элегантная реализация паттерна координатора в Swiftui. Стинсен написан с использованием 100% Swiftui, что заставляет его беспрепятственно работать на устройствах iOS, TVOS, WatchOS и MacOS.
Мы все знаем, что маршрутизация в UIKIT может быть трудно сделать элегантно при работе с приложениями большего размера или при попытке применения архитектурной схемы, такой как MVVM. К сожалению, Swiftui из коробки страдает от многих из тех же проблем, что и Uikit: такие концепции, как NavigationLink живут в Layer View, у нас все еще нет четкой концепции потоков и маршрутов и так далее. Стинсен был создан, чтобы облегчить эти боли, и является реализацией паттерна координатора . Будучи написанным в Swiftui, он полностью кроссплатформенный и использует собственные инструменты, такие как @EnvironmentObject . Цель состоит в том, чтобы Стинсен почувствовал себя недостающим инструментом в Swiftui, что соответствует его стилю кодирования и общим принципам.
Обычно в Swiftui представление должно справиться с добавлением других представлений в стек навигации с использованием NavigationLink . Что у нас есть здесь, так это тесная связь между представлениями, поскольку представление должно знать заранее все другие взгляды, которые он может перемещаться между ними. Кроме того, мнение нарушает принцип единой реализации (SRP). Используя шаблон координатора, представленную сообществу iOS Соруусом Ханлу на конференции NSSpain в 2015 году, мы можем делегировать эту ответственность в более высокий класс: координатор.
Пример с использованием навигационного стека:
final class UnauthenticatedCoordinator : NavigationCoordinatable {
let stack = NavigationStack ( initial : UnauthenticatedCoordinator . start )
@ Root var start = makeStart
@ Route ( . modal ) var forgotPassword = makeForgotPassword
@ Route ( . push ) var registration = makeRegistration
func makeRegistration ( ) -> RegistrationCoordinator {
return RegistrationCoordinator ( )
}
@ ViewBuilder func makeForgotPassword ( ) -> some View {
ForgotPasswordScreen ( )
}
@ ViewBuilder func makeStart ( ) -> some View {
LoginScreen ( )
}
} @Route S определяет все возможные маршруты, которые могут быть выполнены из текущего координатора, и переход, который будет выполнен. Значение на правой стороне - это заводская функция, которая будет выполнена при маршрутизации. Функция может вернуть либо представление Swiftui, либо другой координатор. @Root Другой тип маршрута, который не имеет перехода, и используется для определения первого представления о навигационном стеке координатора, на который ссылается NavigationStack -класс.
Стивен из коробки имеет два разных вида Coordinatable протоколов, которые могут реализовать ваши координаторы:
NavigationCoordinatable - для навигационных потоков. Обязательно оберните их в NavigationViewCoordinator, если вы хотите нажать на навигационный стек.TabCoordinatable - для Tabviews. Кроме того, Stinsen также имеет два координатора, которые вы можете использовать, ViewWrapperCoordinator и NavigationViewCoordinator . ViewWrapperCoordinator - это координатор, который вы можете сразу же использовать подкласс или использовать, чтобы обернуть свой координатор в представление, а NavigationViewCoordinator - это подкласс ViewWrapperCoordinator , который завершает ваш координатор в NavigationView .
Представление для координатора может быть создано с помощью .view() , поэтому, чтобы показать координатору пользователю, вы просто сделаете что -то вроде:
struct StinsenApp : App {
var body : some Scene {
WindowGroup {
MainCoordinator ( ) . view ( )
}
}
} Стинсен можно использовать для питания всего вашего приложения или только части вашего приложения. Вы все еще можете использовать обычные Swiftui NavigationLink S и представлять модальные листы внутри просмотров, управляемых Стинсеном , если вы хотите это сделать.
Используя маршрутизатор, который имеет ссылку как на координатор, так и на представление, мы можем выполнить переходы из представления. Внутри вида маршрутизатор можно получить с помощью @EnvironmentObject . Используя маршрутизатор, который можно перейти на другие маршруты:
struct TodosScreen : View {
@ EnvironmentObject var todosRouter : TodosCoordinator . Router
var body : some View {
List {
/* ... */
}
. navigationBarItems (
trailing : Button (
action : {
// Transition to the screen to create a todo:
todosRouter . route ( to : . createTodo )
} ,
label : {
Image ( systemName : " doc.badge.plus " )
}
)
)
}
} Вы также можете получить маршрутизаторы, ссылающиеся на координаторов, которые появились ранее на дереве. Например, вы можете переключить вкладку с представления, который находится внутри TabView .
Маршрутизация может быть выполнена непосредственно на самом координаторе, который может быть полезен, если вы хотите, чтобы у вашего координатора была какая -то логика, или если вы передаете координатор вокруг:
final class MainCoordinator : NavigationCoordinatable {
@ Root var unauthenticated = makeUnauthenticated
@ Root var authenticated = makeAuthenticated
/* ... */
init ( ) {
/* ... */
cancellable = AuthenticationService . shared . status . sink { [ weak self ] status in
switch status {
case . authenticated ( let user ) :
self ? . root ( . authentiated , user )
case . unauthenticated :
self ? . root ( . unauthentiated )
}
}
}
} Какие действия вы можете выполнить с маршрутизатора/координатора, зависит от вида используемого координатора. Например, используя NavigationCoordinatable , некоторые из функций, которые вы можете выполнить, являются:
popLast - удаляет последний элемент из стека. Обратите внимание, что Стинсен не заботится о том, что представление было представлено модально или натолкнуто, для обоих используется та же функция.pop - Удаляет представление из стека. Эта функция может выполняться только с помощью маршрутизатора, так как только маршрутизатор знает о том, какой вид вы пытаетесь выскочить.popToRoot - очищает стек.root - изменяет корень (т.е. первый вид стека). Если корень уже является активным корнем, ничего не сделает.route - перемещается по другому маршруту.focusFirst - находит указанный маршрут, если он существует в стеке, начиная с первого элемента. Если обнаружится, удалит все после этого.dismissCoordinator - удаляет весь координатор и его связанные дети с дерева.
Клонировать репо и запустите StinsenApp в примерах/приложении, чтобы почувствовать, как можно использовать Стинсен . Stinsenapp работает над iOS, TVOS, WatchOS и MacOS. Он пытается продемонстрировать многие из функций, которые Стинсен доступен для использования. Большая часть кода из этого Readme поступает из приложения примера. Существует также пример, показывающий, как Stinsen можно использовать для применения тестируемой архитектуры MVVM-C в Swiftui, которая доступна в примере/MVVM .
Поскольку доступ @EnvironmentObject может быть доступен только в View , Стинсен предоставляет несколько способов маршрутизации из ViewModel. Вы можете внедрить координатора через «nitializer» или зарегистрировать его при создании и разрешить его в ViewModel с помощью рамки впрыска зависимостей. Это рекомендуемые способы сделать это, поскольку у вас будет максимальный контроль и функциональность.
Другими способами являются прохождение маршрутизатора, используя функцию onAppear :
struct TodosScreen : View {
@ StateObject var viewModel = TodosViewModel ( )
@ EnvironmentObject var projects : TodosCoordinator . Router
var body : some View {
List {
/* ... */
}
. onAppear {
viewModel . router = projects
}
}
} Вы также можете использовать то, что называется RouterStore , чтобы завоевать маршрутизатор. RouterStore сохраняет экземпляр маршрутизатора, и вы можете получить его через пользовательский wroptrapper.
Чтобы получить маршрутизатор:
class LoginScreenViewModel : ObservableObject {
// directly via the RouterStore
var main : MainCoordinator . Router ? = RouterStore . shared . retrieve ( )
// via the RouterObject property wrapper
@ RouterObject
var unauthenticated : Unauthenticated . Router ?
init ( ) {
}
func loginButtonPressed ( ) {
main ? . root ( . authenticated )
}
func forgotPasswordButtonPressed ( ) {
unauthenticated ? . route ( to : . forgotPassword )
}
}Чтобы увидеть этот пример в действии, пожалуйста, проверьте MVVM-APP в примерах/MVVM .
Иногда вы захотите настроить представление, сгенерированное вашим координатором. NavigationCoordinatable и TabCoordinatable имеет customize -функцию, которую вы можете реализовать для этого:
final class AuthenticatedCoordinator : TabCoordinatable {
/* ... */
@ ViewBuilder func customize ( _ view : AnyView ) -> some View {
view
. onReceive ( Services . shared . $authentication ) { authentication in
switch authentication {
case . authenticated :
self . root ( . authenticated )
case . unauthenticated :
self . root ( . unauthenticated )
}
}
}
}
} Существует также ViewWrapperCoordinator , который вы также можете использовать для настройки.
Поскольку большинство функций на координаторе/маршрутизаторе возвращают координатора, вы можете использовать результаты и объединить их для выполнения более продвинутой маршрутизации, если это необходимо. Например, чтобы создать кнопки Swiftui, которые изменят вкладку и выберите конкретный TODO из любой точки приложения после входа в систему:
VStack {
ForEach ( todosStore . favorites ) { todo in
Button ( todo . name ) {
authenticatedRouter
. focusFirst ( . todos )
. child
. popToRoot ( )
. route ( to : . todo , todo . id )
}
}
} AuthenticatedCoordinator на который ссылается authenticatedRouter , является TabCoordinatable , поэтому функция будет:
focusFirst : верните первую вкладку, представленную маршрутом todos и сделайте ее активной вкладкой, если только она не является активной.child : вернет своего ребенка, Todos -tab -это NavigationViewCoordinator , а ребенок NavigationCoordinatable .popToRoot : откинут всех детей, которые могли или не могли присутствовать.route : будет маршрут к маршруту Todo с указанным идентификатором. Поскольку Стинсен использует клавиатуры для представления маршрутов, функции не могут быть созданы типа, а неверные цепочки не могут быть созданы. Это означает: если у вас есть маршрут в A до B и в B до C , приложение не будет компилироваться, если вы попытаетесь в первую очередь маршрутизации от A к C без маршрутизации на B. Кроме того, вы не можете выполнять такие действия, как popToRoot() на TabCoordinatable и так далее.
Используя возвращаемые значения, вы можете легко DeepLink в приложении:
final class MainCoordinator : NavigationCoordinatable {
@ ViewBuilder func customize ( _ view : AnyView ) -> some View {
view . onOpenURL { url in
if let coordinator = self . hasRoot ( . authenticated ) {
do {
// Create a DeepLink-enum
let deepLink = try DeepLink ( url : url , todosStore : coordinator . todosStore )
switch deepLink {
case . todo ( let id ) :
coordinator
. focusFirst ( . todos )
. child
. route ( to : . todo , id )
}
} catch {
print ( error . localizedDescription )
}
}
}
}
} Стинсен поставляется с парой координат для стандартных видов Swiftui. Если вы, например, хотите использовать его для гамбургера-мену, вам нужно создать свой собственный. Проверьте исходный код, чтобы получить вдохновение.
Стинсен поддерживает два способа установки, кокопод и SPM.
Откройте Xcode и ваш проект, нажмите File / Swift Packages / Add package dependency... В Textfield « Введите URL -адрес репозитория пакета », напишите https://github.com/rundfunk47/stinsen и нажмите следующее дважды
Создайте Podfile в корневом каталоге вашего приложения. Добавлять
# Podfile
use_frameworks!
target 'YOUR_TARGET_NAME' do
pod 'Stinsen'
end
DoubleColumnNavigationViewStyle . Причина этого заключается в том, что он не работает так, как ожидалось, из -за проблем с isActive в Swiftui. Обходной путь: используйте UiviewResentable или создайте свою собственную реализацию.В BYVA мы стремимся создать 100% приложение Swiftui, поэтому вполне естественно, что нам нужно было создать координаторную структуру, которая удовлетворяла эти и другие потребности, которые у нас есть. Структура используется в производстве и управляет ~ 50 потоками и ~ 100 экранами. Фреймворк поддерживается @rundfunk47.
Стян короткий на шведском языке для «Мастер станции», а Стинсен - определенная статья «Мастер станции». В разговорной речи термин был в основном использован для обозначения диспетчера поезда, который отвечает за маршрутизацию поездов. Логотип основан на деревянной статуи Stins , расположенной недалеко от вокзала в Линкёпинге, Швеция.
Самое большое изменение в Stinsen V2 заключается в том, что он более безопасен для типа, чем Stinsen V1, что позволяет более простым цепочкам и глубоким связям, среди прочего.
AnyCoordinatable был заменен протоколом. Он не выполняет те же обязанности, что и старый AnyCoordinatable , и не вписывается в более безопасную маршрутизацию версии 2, поэтому удалите ее из своего проекта.route(to: .a) мы используем route(to: .a) ..view() .@Root S и переключайте между ними, используя .root() чтобы получить одинаковую функциональность.Стинсен выпускается по лицензии MIT. Смотрите лицензию для получения дополнительной информации.