Implementação simples, poderosa e elegante do padrão de coordenador em Swiftui. Stinsen é escrito usando 100% Swiftui, o que o faz funcionar perfeitamente em dispositivos iOS, TVOS, WatchOS e MacOS.
Todos sabemos que o roteamento no UIKIT pode ser difícil de fazer elegantemente ao trabalhar com aplicações de um tamanho maior ou ao tentar aplicar um padrão arquitetônico como o MVVM. Infelizmente, Swiftui fora da caixa sofre de muitos dos mesmos problemas que o Uikit: conceitos como NavigationLink ao vivo na camada de vista, ainda não temos conceito claro de fluxos e rotas, e assim por diante. Stinsen foi criado para aliviar essas dores e é uma implementação do padrão de coordenador . Sendo escrito em Swiftui, ele é completamente transversal e usa as ferramentas nativas, como @EnvironmentObject . O objetivo é fazer com que Stinsen pareça uma ferramenta ausente em Swiftui, em conformidade com seu estilo de codificação e princípios gerais.
Normalmente, em Swiftui, a vista deve lidar com a adição de outras vistas à pilha de navegação usando NavigationLink . O que temos aqui é um acoplamento apertado entre as vistas, pois a visualização deve saber com antecedência todas as outras visualizações que ele pode navegar. Além disso, a visão está violando o princípio de responsabilidade única (SRP). Usando o padrão do coordenador, apresentado à comunidade iOS por Soroush Khanlou na conferência NSSPAIN em 2015, podemos delegar essa responsabilidade a uma classe superior: o coordenador.
Exemplo usando uma pilha de navegação:
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 ( )
}
} O @Route S define todas as rotas possíveis que podem ser executadas no coordenador atual e na transição que será executada. O valor no lado direito é a função de fábrica que será executada ao rotear. A função pode retornar uma visualização Swiftui ou outro coordenador. O @Root Outro tipo de rota que não tem transição e usada para definir a primeira visão da pilha de navegação do coordenador, que é referenciada pela classe NavigationStack .
Stinsen Out of the Box possui dois tipos diferentes de protocolos Coordinatable que seus coordenadores podem implementar:
NavigationCoordinatable - para fluxos de navegação. Certifique -se de envolvê -los em um NavigationVewCoordinator se desejar empurrar a pilha de navegação.TabCoordinatable - PARA TABVIELS. Além disso, o Stinsen também possui dois coordenadores que você pode usar, ViewWrapperCoordinator e NavigationViewCoordinator . ViewWrapperCoordinator é um coordenador que você pode subclasse ou usar imediatamente para embrulhar seu coordenador em uma visualização, e NavigationViewCoordinator é uma subclasse ViewWrapperCoordinator que envolve seu coordenador em um NavigationView .
A visualização para o coordenador pode ser criada usando .view() ; portanto, para mostrar um coordenador ao usuário, você faria algo como:
struct StinsenApp : App {
var body : some Scene {
WindowGroup {
MainCoordinator ( ) . view ( )
}
}
} O Stinsen pode ser usado para alimentar todo o seu aplicativo ou apenas partes do seu aplicativo. Você ainda pode usar o Swiftui NavigationLink s usual e apresentar folhas modais dentro de vistas gerenciadas por Stinsen , se desejar fazê -lo.
Usando um roteador, que tem uma referência ao coordenador e à vista, podemos executar transições a partir de uma visualização. Dentro da vista, o roteador pode ser buscado usando @EnvironmentObject . Usando o roteador, pode -se fazer a transição para outras rotas:
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 " )
}
)
)
}
} Você também pode buscar roteadores de referência a coordenadores que apareceram anteriormente na árvore. Por exemplo, convém alternar a guia de uma exibição que está dentro do TabView .
O roteamento pode ser realizado diretamente no próprio coordenador, o que pode ser útil se você deseja que seu coordenador tenha alguma lógica ou se você passar no coordenador:
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 )
}
}
}
} Quais ações você pode executar no roteador/coordenador depende do tipo de coordenador usado. Por exemplo, usando um NavigationCoordinatable , algumas das funções que você pode executar são:
popLast - Remove o último item da pilha. Observe que Stinsen não se importa se a visualização foi apresentada modalmente ou empurrada, a mesma função é usada para ambos.pop - Remove a vista da pilha. Essa função só pode ser desempenhada por um roteador, pois apenas o roteador sabe sobre qual visão você está tentando aparecer.popToRoot - limpa a pilha.root - Altera a raiz (ou seja, a primeira visão da pilha). Se a raiz já for a raiz ativa, não fará nada.route - navega para outra rota.focusFirst - encontra a rota especificada se existir na pilha, começando pelo primeiro item. Se encontrado, removerá tudo depois disso.dismissCoordinator - exclui todo o coordenador e são crianças associadas da árvore.
Clone o repositório e execute o StinsenApp em exemplos/aplicativos para ter uma ideia de como o Stinsen pode ser usado. StinsenApp funciona em iOS, TVOS, WatchOS e MacOS. Ele tenta mostrar muitos dos recursos que Stinsen está disponível para você usar. A maior parte do código deste ReadMe vem do aplicativo de amostra. Há também um exemplo mostrando como o Stinsen pode ser usado para aplicar uma arquitetura MVVM-C testável em Swiftui, que está disponível no Exemplo/MVVM .
Como @EnvironmentObject só pode ser acessado em uma View , o Stinsen fornece algumas maneiras de rotear a partir do viewmodel. Você pode injetar o coordenador através do “Nitializer ou registrá -lo na criação e resolvê -lo no ViewModel através de uma estrutura de injeção de dependência. Essas são as maneiras recomendadas de fazer isso, pois você terá o máximo controle e funcionalidade.
Outras maneiras estão passando o roteador usando a função onAppear :
struct TodosScreen : View {
@ StateObject var viewModel = TodosViewModel ( )
@ EnvironmentObject var projects : TodosCoordinator . Router
var body : some View {
List {
/* ... */
}
. onAppear {
viewModel . router = projects
}
}
} Você também pode usar o que é chamado de RouterStore para recuperar o roteador. O RouterStore salva a instância do roteador e você pode obtê -lo por meio de um PropertyWrapper personalizado.
Para recuperar um roteador:
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 )
}
}Para ver este exemplo em ação, verifique o MVVM-APP em exemplos/MVVM .
Às vezes, você deseja personalizar a visualização gerada pelo seu coordenador. NavigationCoordinatable e TabCoordinatable têm uma função customize que você pode implementar para fazê -lo:
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 )
}
}
}
}
} Há também um ViewWrapperCoordinator que você pode usar para personalizar também.
Como a maioria das funções no coordenador/roteador retorna um coordenador, você pode usar os resultados e acorrentá -los para realizar o roteamento mais avançado, se necessário. Por exemplo, para criar um botão Swiftui que altere a guia e selecione um TODO específico de qualquer lugar do aplicativo após o login:
VStack {
ForEach ( todosStore . favorites ) { todo in
Button ( todo . name ) {
authenticatedRouter
. focusFirst ( . todos )
. child
. popToRoot ( )
. route ( to : . todo , todo . id )
}
}
} O AuthenticatedCoordinator referenciado pelo authenticatedRouter é um TabCoordinatable , então a função será:
focusFirst : retorne a primeira guia representada pelo todos da rota e faça com que ela seja a guia ativa, a menos que já seja a ativa.child : Vai devolver seu filho, o Todos -TAB é um NavigationViewCoordinator e a criança é a NavigationCoordinatable .popToRoot : Estabelecerá crianças que possam ou não estar presentes.route : irá percorrer a Todo com o ID especificado. Como Stinsen usa o Keypaths para representar as rotas, as funções são seguras de tipo e as cadeias inválidas não podem ser criadas. Isso significa: se você tiver uma rota em A a B e em B a C , o aplicativo não será compilado se você tentar rotear de A a C sem rotear para B primeiro. Além disso, você não pode executar ações como popToRoot() em um TabCoordinatable e assim por diante.
Usando os valores retornados, você pode facilmente se envolver no aplicativo:
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 )
}
}
}
}
} O Stinsen vem com alguns coordenatáveis para visualizações padrão do SwiftUI. Se você, por exemplo, deseja usá-lo para um hambúrguer-menu, precisará criar o seu próprio. Verifique o código de origem para obter alguma inspiração.
Stinsen suporta duas maneiras de instalação, Cocoapods e SPM.
Abra o Xcode e seu projeto, clique em File / Swift Packages / Add package dependency... No campo de texto " Enter Package Repository URL ", escreva https://github.com/rundfunk47/stinsen e pressione a próxima duas vezes
Crie um Podfile no diretório raiz do seu aplicativo. Adicionar
# Podfile
use_frameworks!
target 'YOUR_TARGET_NAME' do
pod 'Stinsen'
end
DoubleColumnNavigationViewStyle . A razão para isso é que ele não funciona conforme o esperado devido a problemas com isActive em Swiftui. Solução alternativa: use o uiviewRepresentable ou crie sua própria implementação.Na BYVA, nos esforçamos para criar um aplicativo 100% swifttu, por isso é natural que precisássemos criar uma estrutura de coordenador que atenda a essas e outras necessidades que temos. A estrutura é usada na produção e gerencia ~ 50 fluxos e ~ 100 telas. A estrutura é mantida por @runfunk47.
Stins é curto em sueco para "Station Master", e Stinsen é o artigo definitivo, "The Station Master". Colocialmente, o termo foi usado principalmente para se referir ao despachante de trem, responsável pelo roteamento dos trens. O logotipo é baseado em uma estátua de madeira de um Stins localizado perto da estação de trem em Linköping, na Suécia.
A maior mudança no Stinsen V2 é que ela é mais segura do que o Stinsen V1, o que permite um encadeamento e uma ligação profunda, entre outras coisas.
AnyCoordinatable foi substituído por um protocolo. Ele não desempenha as mesmas tarefas que o antigo AnyCoordinatable e não se encaixa no roteamento mais seguro da versão 2, então remova-o do seu projeto.route(to: .a) usamos route(to: .a) ..view() .@Root s e alterne entre eles usando .root() para obter a mesma funcionalidade.Stinsen é liberado sob uma licença do MIT. Consulte a licença para obter mais informações.