Implémentation simple, puissante et élégante du modèle de coordinateur dans Swiftui. Stinsen est écrit à l'aide de 100% Swiftui, ce qui le fait fonctionner de manière transparente sur les appareils iOS, TVOS, WatchOS et MacOS.
Nous savons tous que le routage dans UIKIT peut être difficile à faire avec élégance lorsque vous travaillez avec des applications de plus grande taille ou lorsque vous essayez d'appliquer un modèle architectural tel que MVVM. Malheureusement, Swiftui Out of the Box souffre de plusieurs des mêmes problèmes que Uikit: des concepts tels que NavigationLink en direct dans la couche de vue, nous n'avons toujours pas de concept clair de flux et de voies, etc. Stinsen a été créé pour atténuer ces douleurs et est une mise en œuvre du modèle de coordinateur . Écrit dans Swiftui, il est entièrement multiplateforme et utilise les outils natifs tels que @EnvironmentObject . L'objectif est de faire en sorte que Stinsen se sente comme un outil manquant dans Swiftui, conforme à son style de codage et à ses principes généraux.
Normalement, dans Swiftui, la vue doit gérer l'ajout d'autres vues à la pile de navigation à l'aide de NavigationLink . Ce que nous avons ici est un couplage serré entre les vues, car la vue doit savoir à l'avance toutes les autres vues qu'elles peuvent naviguer. De plus, l'opinion est en violation du principe de responsabilité unique (SRP). En utilisant le modèle de coordinateur, présenté à la communauté iOS par Soroush Khanlou lors de la conférence NSSpain en 2015, nous pouvons déléguer cette responsabilité à une classe supérieure: le coordinateur.
Exemple à l'aide d'une pile de navigation:
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 ( )
}
} Le @Route S définit toutes les routes possibles qui peuvent être effectuées à partir du coordinateur actuel et de la transition qui sera effectuée. La valeur à droite est la fonction d'usine qui sera exécutée lors du routage. La fonction peut renvoyer une vue Swiftui ou un autre coordinateur. Le @Root un autre type d'itinéraire qui n'a pas de transition et utilisé pour définir la première vue de la pile de navigation du coordinateur, qui est référencée par la classe NavigationStack .
STINSEN OUT OF THE BOX possède deux types différents de protocoles Coordinatable que vos coordinateurs peuvent mettre en œuvre:
NavigationCoordinatable - Pour les flux de navigation. Assurez-vous de les envelopper dans un NavigationViewCoordinator si vous souhaitez pousser sur la pile de navigation.TabCoordinatable - pour TabViews. De plus, StiNsen a également deux coordinateurs que vous pouvez utiliser, ViewWrapperCoordinator et NavigationViewCoordinator . ViewWrapperCoordinator est un coordinateur que vous pouvez soit sous-classe ou utiliser immédiatement pour envelopper votre coordinateur dans une vue, et NavigationViewCoordinator est une sous-classe ViewWrapperCoordinator qui enveloppe votre coordinateur dans un NavigationView .
La vue pour le coordinateur peut être créée à l'aide de .view() , donc pour montrer un coordinateur à l'utilisateur, vous feriez simplement quelque chose comme:
struct StinsenApp : App {
var body : some Scene {
WindowGroup {
MainCoordinator ( ) . view ( )
}
}
} Stinsen peut être utilisé pour alimenter l'ensemble de votre application, ou simplement des parties de votre application. Vous pouvez toujours utiliser les Swiftui NavigationLink habituels et présenter des feuilles modales à l'intérieur des vues gérées par Stinsen , si vous le souhaitez.
En utilisant un routeur, qui a une référence à la fois au coordinateur et à la vue, nous pouvons effectuer des transitions à partir d'une vue. À l'intérieur de la vue, le routeur peut être récupéré à l'aide de @EnvironmentObject . En utilisant le routeur, on peut passer à d'autres itinéraires:
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 " )
}
)
)
}
} Vous pouvez également aller chercher des routeurs qui référennent des coordinateurs qui sont apparus plus tôt dans l'arbre. Par exemple, vous souhaiterez peut-être changer l'onglet à partir d'une vue qui se trouve à l'intérieur du TabView .
Le routage peut être effectué directement sur le coordinateur lui-même, ce qui peut être utile si vous souhaitez que votre coordinateur ait une logique, ou si vous passez le coordinateur:
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 )
}
}
}
} Les actions que vous pouvez effectuer à partir du routeur / coordinateur dépend du type de coordinateur utilisé. Par exemple, en utilisant un NavigationCoordinatable , certaines des fonctions que vous pouvez remplir sont:
popLast - Supprime le dernier élément de la pile. Notez que Stinsen ne se soucie pas si la vue a été présentée modalement ou poussée, la même fonction est utilisée pour les deux.pop - supprime la vue de la pile. Cette fonction ne peut être exécutée que par un routeur, car seul le routeur sait sur quelle vue vous essayez de faire éclater.popToRoot - efface la pile.root - change la racine (c'est-à-dire la première vue de la pile). Si la racine est déjà la racine active, ne fera rien.route - navigue vers un autre itinéraire.focusFirst - trouve l'itinéraire spécifié s'il existe dans la pile, à partir du premier élément. S'il est trouvé, vous supprimera tout après cela.dismissCoordinator - Supprime l'ensemble du coordinateur et ses enfants associés de l'arbre.
Clone le repo et exécutez le StiNsenApp dans des exemples / application pour avoir une idée de la façon dont Stinsen peut être utilisé. STINENAPP travaille sur iOS, TVOS, Watchos et MacOS. Il tente de présenter bon nombre des fonctionnalités que StiNsen a disponibles pour vous. La majeure partie du code de cette lecture provient de l'exemple d'application. Il existe également un exemple montrant comment StiNsen peut être utilisé pour appliquer une architecture MVVM-C testable dans SwiftUi, qui est disponible en exemple / MVVM .
Étant donné que @EnvironmentObject ne peut être accessible que dans une View , Stinsen fournit quelques façons de router depuis le ViewModel. Vous pouvez injecter le coordinateur par le biais de l'unitialiseur, ou l'enregistrer à la création et le résoudre dans ViewModel via un cadre d'injection de dépendance. Ce sont les moyens recommandés de le faire, car vous aurez un contrôle et une fonctionnalité maximum.
D'autres façons consistent à passer le routeur à l'aide de la fonction onAppear :
struct TodosScreen : View {
@ StateObject var viewModel = TodosViewModel ( )
@ EnvironmentObject var projects : TodosCoordinator . Router
var body : some View {
List {
/* ... */
}
. onAppear {
viewModel . router = projects
}
}
} Vous pouvez également utiliser ce qu'on appelle le RouterStore pour retarder le routeur. Le RouterStore enregistre l'instance du routeur et vous pouvez l'obtenir via un propriété personnalisé.
Pour récupérer un routeur:
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 )
}
}Pour voir cet exemple en action, veuillez consulter le MVVM-App dans des exemples / MVVM .
Parfois, vous souhaitez personnaliser la vue générée par votre coordinateur. NavigationCoOrDinateable et TabCoOrDinAtable ont une fonction de customize que vous pouvez implémenter pour ce faire:
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 )
}
}
}
}
} Il existe également un ViewWrapperCoordinator que vous pouvez également utiliser pour personnaliser.
Étant donné que la plupart des fonctions sur le coordinateur / routeur renvoient un coordinateur, vous pouvez utiliser les résultats et les enchaîner ensemble pour effectuer un routage plus avancé, si nécessaire. Par exemple, pour créer des boutons Swiftui qui modifieront l'onglet et sélectionneront un TODO spécifique de n'importe où dans l'application après la connexion:
VStack {
ForEach ( todosStore . favorites ) { todo in
Button ( todo . name ) {
authenticatedRouter
. focusFirst ( . todos )
. child
. popToRoot ( )
. route ( to : . todo , todo . id )
}
}
} L' AuthenticatedCoordinator référencée par le authenticatedRouter est un TabCoordinatable , de sorte que la fonction sera:
focusFirst : renvoyez le premier onglet représenté par l'itinéraire todos et faites-en l'onglet actif, sauf si c'est déjà celui actif.child : va retourner son enfant, le Todos -Tab est un NavigationViewCoordinator et l'enfant est le NavigationCoordinatable .popToRoot : Va éclater tous les enfants qui pourraient ou non être présents.route : se traduira vers l'itinéraire Todo avec l'ID spécifié. Étant donné que STINEN utilise des forfaits pour représenter les itinéraires, les fonctions sont des chaînes de type et non valides ne peuvent pas être créées. Cela signifie: si vous avez un itinéraire en A vers B et en B à C , l'application ne compilera pas si vous essayez d'achever de A à C sans routage vers B en premier. De plus, vous ne pouvez pas effectuer des actions telles que popToRoot() sur un TabCoordinatable et ainsi de suite.
En utilisant les valeurs renvoyées, vous pouvez facilement de profondeur dans l'application:
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 )
}
}
}
}
} Stinsen est livré avec quelques vues coordonnées pour des vues Swiftui standard. Si vous souhaitez par exemple l'utiliser pour un hamburger-menu, vous devez créer le vôtre. Vérifiez le code source pour vous inspirer.
Stinsen prend en charge deux façons d'installation, Cocoapods et SPM.
Ouvrez Xcode et votre projet, cliquez sur File / Swift Packages / Add package dependency... Dans TextField " Entrez URL du référentiel du package ", écrivez https://github.com/rundfunk47/stinsen et appuyez sur deux fois
Créez un Podfile dans le répertoire racine de votre application. Ajouter
# Podfile
use_frameworks!
target 'YOUR_TARGET_NAME' do
pod 'Stinsen'
end
DoubleColumnNavigationViewStyle . La raison en est que cela ne fonctionne pas comme prévu en raison de problèmes avec isActive dans Swiftui. Solution: utilisez uiViewRepresentable ou créez votre propre implémentation.Chez BYVA, nous nous efforçons de créer une application 100% SwiftUi, il est donc naturel que nous devions créer un cadre de coordinateur qui a satisfait ce besoin et d'autres besoins que nous avons. Le cadre est utilisé dans la production et gère ~ 50 flux et ~ 100 écrans. Le cadre est maintenu par @ rundfunk47.
Stins est court en suédois pour "Master de la station", et Stinsen est l'article défini, "The Station Master". Farloque, le terme a été principalement utilisé pour se référer au répartiteur des trains, qui est responsable de l'acheminement des trains. Le logo est basé sur une statue en bois d'un STITS qui est situé près de la gare de Linköping, en Suède.
Le plus grand changement dans Stinesen V2 est qu'il est plus sécurisé que Stinesen V1, ce qui permet un chaînage plus facile et une liaison profonde, entre autres.
AnyCoordinatable a été remplacé par un protocole. Il n'effectue pas les mêmes tâches que l'ancien AnyCoordinatable et ne correspond pas au routage plus sécurisé de la version 2, alors supprimez-le de votre projet.route(to: .a) nous utilisons route(to: .a) ..view() .@Root s et basculez entre eux en utilisant .root() pour obtenir les mêmes fonctionnalités.Stinsen est libéré sous une licence du MIT. Voir la licence pour plus d'informations.