Implementación simple, poderosa y elegante del patrón del coordinador en Swiftui. Stinsen está escrito usando 100% Swiftui, lo que hace que funcione sin problemas a través de dispositivos iOS, TVOS, WatchOS y MacOS.
Todos sabemos que el enrutamiento en Uikit puede ser difícil de hacer elegantemente cuando trabaja con aplicaciones de un tamaño más grande o al intentar aplicar un patrón arquitectónico como MVVM. Desafortunadamente, Swiftui fuera de la caja sufre de muchos de los mismos problemas que Uikit: conceptos como NavigationLink viven en la capa de visión, todavía no tenemos un concepto claro de flujos y rutas, etc. Stinsen fue creado para aliviar estos dolores, y es una implementación del patrón del coordinador . Al ser escrito en Swiftui, es completamente multiplataforma y utiliza las herramientas nativas como @EnvironmentObject . El objetivo es hacer que Stinsen se sienta como una herramienta faltante en Swiftui, conforme a su estilo de codificación y principios generales.
Normalmente en Swiftui, la vista debe manejar agregar otras vistas a la pila de navegación utilizando NavigationLink . Lo que tenemos aquí es un acoplamiento ajustado entre las vistas, ya que la vista debe saber de antemano todas las demás vistas que puede navegar. Además, la opinión está en violación del Principio de responsabilidad única (SRP). Utilizando el patrón del coordinador, presentado a la comunidad de iOS por Soroush Khanlou en la Conferencia NSSPAIN en 2015, podemos delegar esta responsabilidad a una clase superior: el Coordinador.
Ejemplo Uso de una pila de navegación:
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 ( )
}
} El @Route S define todas las rutas posibles que se pueden realizar desde el coordinador actual y la transición que se realizará. El valor en el lado derecho es la función de fábrica que se ejecutará al enrutar. La función puede devolver una vista swiftui u otro coordinador. El @Root otro tipo de ruta que no tiene transición, y se usa para definir la primera vista de la pila de navegación del coordinador, a la que la clase NavigationStack hace referencia.
Stinsen fuera de la caja tiene dos tipos diferentes de protocolos Coordinatable que sus coordinadores pueden implementar:
NavigationCoordinatable - para flujos de navegación. Asegúrese de envolverlos en un coordinador de navegaciónviewview si desea presionar la pila de navegación.TabCoordinatable - para TabViews. Además, Stinsen también tiene dos coordinadores que puede usar, ViewWrapperCoordinator y NavigationViewCoordinator . ViewWrapperCoordinator es un coordinador que puede subclase o usar de inmediato para envolver a su coordinador en una vista, y NavigationViewCoordinator es una subclase ViewWrapperCoordinator que envuelve su coordinador en una NavigationView .
La vista para el coordinador se puede crear usando .view() , por lo que para mostrar un coordinador al usuario, simplemente haría algo como:
struct StinsenApp : App {
var body : some Scene {
WindowGroup {
MainCoordinator ( ) . view ( )
}
}
} Stinsen se puede usar para alimentar toda su aplicación, o simplemente partes de su aplicación. Todavía puede usar la NavigationLink swiftui habitual sy presentar sábanas modales dentro de las vistas administradas por Stinsen , si lo desea.
Usando un enrutador, que tiene una referencia tanto al coordinador como a la vista, podemos realizar transiciones desde una vista. Dentro de la vista, el enrutador se puede obtener usando @EnvironmentObject . Usando el enrutador uno puede hacer la transición a otras rutas:
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 " )
}
)
)
}
} También puede obtener enrutadores que hacen referencia a coordinadores que aparecieron anteriormente en el árbol. Por ejemplo, es posible que desee cambiar la pestaña desde una vista que está dentro de TabView .
El enrutamiento se puede realizar directamente en el coordinador en sí, lo que puede ser útil si desea que su coordinador tenga algo de lógica, o si pasa el coordinador:
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 )
}
}
}
} Las acciones que puede realizar desde el enrutador/coordinador depende del tipo de coordinador utilizado. Por ejemplo, utilizando un NavigationCoordinatable , algunas de las funciones que puede realizar son:
popLast : elimina el último elemento de la pila. Tenga en cuenta que a Stinsen no le importa si la vista se presentó modalmente o se empujó, se usa la misma función para ambos.pop : elimina la vista de la pila. Esta función solo puede ser realizada por un enrutador, ya que solo el enrutador sabe qué vista está tratando de explotar.popToRoot : borra la pila.root : cambia la raíz (es decir, la primera vista de la pila). Si la raíz ya es la raíz activa, no hará nada.route : navega a otra ruta.focusFirst : encuentra la ruta especificada si existe en la pila, comenzando desde el primer elemento. Si se encuentra, eliminará todo después de eso.dismissCoordinator : elimina todo el coordinador y sus niños asociados del árbol.
Clonar el repositorio y ejecute StinsenApp en ejemplos/aplicación para tener una idea de cómo se puede usar Stinsen . Stinsenapp trabaja en iOS, Tvos, Watchos y MacOS. Intenta mostrar muchas de las características que Stinsen tiene disponible para que lo use. La mayor parte del código de este ReadMe proviene de la aplicación de muestra. También hay un ejemplo que muestra cómo Stinsen se puede usar para aplicar una arquitectura MVVM-C comprobable en Swiftui, que está disponible en Ejemplo/MVVM .
Dado que @EnvironmentObject solo se puede acceder dentro de una View , Stinsen proporciona un par de formas de enrutamiento desde ViewModel. Puede inyectar el coordinador a través del "Nitializador, o registrarlo en la creación y resolverlo en el Modelo View a través de un marco de inyección de dependencia. Estas son las formas recomendadas de hacer esto, ya que tendrá el máximo control y funcionalidad.
Otras formas están pasando el enrutador utilizando la función onAppear :
struct TodosScreen : View {
@ StateObject var viewModel = TodosViewModel ( )
@ EnvironmentObject var projects : TodosCoordinator . Router
var body : some View {
List {
/* ... */
}
. onAppear {
viewModel . router = projects
}
}
} También puede usar lo que se llama RouterStore para retener el enrutador. RouterStore guarda la instancia del enrutador y puede obtenerla a través de un PropertyWrapper personalizado.
Para recuperar un enrutador:
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 ejemplo en acción, consulte el MVVM-app en ejemplos/MVVM .
A veces desea personalizar la vista generada por su coordinador. NavigationCoordinatable y TabCoordinatable tienen una función customize que puede implementar para hacerlo:
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 )
}
}
}
}
} También hay un ViewWrapperCoordinator que también puede usar para personalizar.
Dado que la mayoría de las funciones en el coordinador/enrutador devuelven un coordinador, puede usar los resultados y encadenarlos para realizar un enrutamiento más avanzado, si es necesario. Por ejemplo, para crear un botón Swiftui que cambie la pestaña y seleccione un TODO específico desde cualquier lugar de la aplicación después de iniciar sesión:
VStack {
ForEach ( todosStore . favorites ) { todo in
Button ( todo . name ) {
authenticatedRouter
. focusFirst ( . todos )
. child
. popToRoot ( )
. route ( to : . todo , todo . id )
}
}
} El AuthenticatedCoordinator a la que se hace referencia el authenticatedRouter es un TabCoordinatable , por lo que la función:
focusFirst : Devuelva la primera pestaña representada por la ruta todos y conviértela en la pestaña Activa, a menos que ya sea la activa.child : devolverá su niño, el Todos -TAB es un NavigationViewCoordinator y el niño es el NavigationCoordinatable .popToRoot : aparecerá a cualquier niño que pueda o no haber estado presente.route : Ruta a la ruta Todo con la identificación especificada. Dado que Stinsen usa Kypaths para representar las rutas, las funciones son de tipo seguras y no se pueden crear cadenas no válidas. Esto significa: si tiene una ruta en A a B y en B a C , la aplicación no se compilará si intenta enrutar de A a C sin enrutar primero a B. Además, no puede realizar acciones como popToRoot() en un TabCoordinatable , etc.
Usando los valores devueltos, puede obtener fácilmente profundamente dentro de la aplicación:
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 viene con un par de coordinables para vistas estándar de Swiftui. Si, por ejemplo, desea usarlo para un hamburguesa-Menú, debe crear el suyo propio. Verifique el código fuente para obtener algo de inspiración.
Stinsen admite dos formas de instalación, Cocoapods y SPM.
Abra Xcode y su proyecto, haga clic en File / Swift Packages / Add package dependency... En el campo de texto " Ingrese la URL del repositorio del paquete ", escriba https://github.com/rundfunk47/stinsen y presione el siguiente dos veces
Cree un Podfile en el directorio raíz de su aplicación. Agregar
# Podfile
use_frameworks!
target 'YOUR_TARGET_NAME' do
pod 'Stinsen'
end
DoubleColumnNavigationViewStyle . La razón de esto es que no funciona como se esperaba debido a problemas con isActive en Swiftui. Solución alternativa: use UIViewRepresentable o cree su propia implementación.En BYVA nos esforzamos por crear una aplicación 100% Swiftui, por lo que es natural que necesitemos crear un marco de coordinadores que satisfaga esta y otras necesidades que tenemos. El marco se usa en producción y administra ~ 50 flujos y ~ 100 pantallas. El marco es mantenido por @RunDFunk47.
Stins es corto en sueco para "Station Master", y Stinsen es el artículo definitivo, "The Station Master". Coloquialmente el término se utilizó principalmente para referirse al despachador de trenes, que es responsable de enrutar los trenes. El logotipo se basa en una estatua de madera de una punta que se encuentra cerca de la estación de tren en Linköping, Suecia.
El cambio más grande en Stinsen V2 es que es más seguro de tipo que Stinsen V1, lo que permite un encadenamiento y una liquidación profunda, entre otras cosas.
AnyCoordinatable ha sido reemplazado por un protocolo. No realiza las mismas tareas que el antiguo AnyCoordinatable y no encaja con el enrutamiento más seguro de tipo de versión 2, así que elimínelo de su proyecto.route(to: .a) usamos route(to: .a) ..view() .@Root sy cambie entre ellos usando .root() para obtener la misma funcionalidad.Stinsen se libera bajo una licencia del MIT. Vea la licencia para más información.