En un artículo anterior , hablé sobre el enfoque que usamos para componer y navegar entre los controladores de vista en varias aplicaciones en las que trabajo, lo que, como resultado, resultó en una biblioteca RouteComposer separada. Recibí una cantidad significativa de comentarios agradables sobre el artículo anterior y algunos consejos prácticos, lo que me llevó a escribir otro que explicaría un poco más cómo configurar la biblioteca. Debajo del corte, intentaré distinguir algunas de las configuraciones más comunes.

Cómo el enrutador analiza la configuración
Para comenzar, considere cómo el enrutador analiza la configuración que escribió. Tome el ejemplo del artículo anterior:
let productScreen = StepAssembly(finder: ClassFinder(options: [.current, .visible]), factory: ProductViewControllerFactory()) .using(UINavigationController.pushToNavigation()) .from(SingleContainerStep(finder: NilFinder(), factory: NavigationControllerFactory())) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble()
El enrutador pasará por la cadena de pasos comenzando desde el primer momento, hasta que uno de los pasos (usando el Finder
proporcionado) "informa" que el UIViewController
deseado ya UIViewController
presente en la pila. (Por ejemplo, GeneralStep.current()
garantizado de estar presente en la pila del controlador). Luego, el enrutador comenzará a retroceder a lo largo de la cadena de pasos creando los UIViewController
requeridos usando los UIViewController
proporcionados e integrándolos usando los Action
s especificados. Gracias a la verificación de tipos incluso en la etapa de compilación, la mayoría de las veces, no podrá usar UITabBarController.addTab
incompatibles con el Fabric
proporcionado (es decir, no podrá usar UITabBarController.addTab
en el controlador de UITabBarController.addTab
creado por NavigationControllerFactory
).
Si imagina la configuración descrita anteriormente, entonces si solo tiene un determinado ProductViewController
en la pantalla, se realizarán los siguientes pasos:
ClassFinder
no encontrará ProductViewController
y el enrutador continuaráNilFinder
nunca encontrará nada y el enrutador seguirá adelanteGeneralStep.current
siempre devolverá el UIViewController
superior en la pila.- Iniciar
UIViewController
encuentra, el enrutador volverá - Construye un
UINavigationController
usando el `NavigationControllerFactory - Lo mostrará modalmente usando
GeneralAction.presentModally
ProductViewController
ProductViewController ProductViewControllerFactory
- Integra el
ProductViewController
creado en el UINavigationController
anterior usando UINavigationController.pushToNavigation
- Terminar la navegación
NB: Debe entenderse que en realidad es imposible mostrar un UINavigationController
sin algún UIViewController
dentro de él. Por lo tanto, el router realizará los pasos 5 a 8 en un orden ligeramente diferente. Pero no deberías pensarlo. La configuración se describe secuencialmente.
Una buena práctica al escribir una configuración es suponer que el usuario puede estar ubicado en cualquier lugar de su aplicación en ese momento y, de repente, recibe un mensaje push con una solicitud para llegar a la pantalla que está describiendo, y tratar de responder la pregunta: "¿Cómo debería comportarse la aplicación? ? "," ¿Cómo se comportará el Finder
en la configuración que estoy describiendo? ". Si se tienen en cuenta todas estas preguntas, obtendrá una configuración que garantiza mostrar al usuario la pantalla deseada donde quiera que esté. Y este es el requisito principal para las aplicaciones modernas de los equipos involucrados en la comercialización y la atracción de usuarios.
StackIteratingFinder
y sus opciones:
Puede implementar el concepto Finder
de la forma que considere más aceptable. Sin embargo, la forma más fácil es recorrer en iteración el gráfico de los controladores de vista en la pantalla. Para simplificar este objetivo, la biblioteca proporciona StackIteratingFinder
y varias implementaciones que se encargarán de esta tarea. Solo tiene que responder la pregunta: ¿es este el UIViewController
que espera?
Para influir en el comportamiento de StackIteratingFinder
y decirle en qué partes del gráfico (pila) de los controladores de vista desea que busque, puede especificar una combinación de SearchOptions
al crearlo. Y deberían detenerse con más detalle:
current
: el controlador de vista superior en la pila. (El que es el rootViewController
la UIWindow
o el que se muestra modalmente en la parte superior)visible
: si el UIViewController
es un contenedor, mire en su UIViewController
visible (por ejemplo: UINavigationController
siempre tiene un UIViewController
visible, UISplitController
puede tener uno o dos dependiendo de cómo se presente).contained
: en caso de que el UIViewController
sea un contenedor, busque en todos sus UIViewController
anidados (por ejemplo: UIViewController
todos los controladores de vista del UINavigationController
incluido el visible)presenting
: Buscar también en todos los UIViewController
ah debajo del UIViewController
(si hay, por supuesto)presented
: Busque UIViewController
para el que se proporciona (para StackIteratingFinder
esta opción no tiene sentido, ya que siempre comienza desde arriba)
La siguiente figura puede hacer que la explicación anterior sea más obvia:

Recomendaría familiarizarse con el concepto de contenedores en un artículo anterior.
Ejemplo Si desea que su Finder
busque un AccountViewController
en toda la pila, pero solo entre los UIViewController
visibles, esto debería escribirse así:
ClassFinder<AccountViewController, Any?>(options: [.current, .visible, .presenting])
NB Si, por algún motivo, la configuración proporcionada es poca, siempre puede escribir fácilmente su implementación de Finder
a. Un ejemplo estará en este artículo.
Pasemos, de hecho, a los ejemplos.
Ejemplos de configuraciones con explicaciones.
Tengo un cierto UIViewController
, que es el rootViewController
UIWindow
, y quiero HomeViewController
con un cierto HomeViewController
al final de la navegación:
let screen = StepAssembly( finder: ClassFinder<HomeViewController, Any?>(), factory: XibFactory()) .using(GeneralAction.replaceRoot()) .from(GeneralStep.root()) .assemble()
XibFactory
cargará HomeViewController
desde el archivo xib de HomeViewController.xib
No olvide que si usa implementaciones abstractas de Finder
y Factory
en combinación, debe especificar el tipo de UIViewController
y el contexto para al menos una de las entidades: ClassFinder<HomeViewController, Any?>
¿Qué sucede si, en el ejemplo anterior, reemplazo GeneralStep.root
con GeneralStep.current
?
La configuración funcionará hasta que se llame en el momento en que haya algún UIViewController
modal en la pantalla. En este caso, GeneralAction.replaceRoot
no podrá reemplazar el controlador raíz, ya que hay un controlador modal por encima y el enrutador informará un error. Si desea que esta configuración funcione de todos modos, debe explicarle al enrutador que desea que GeneralAction.replaceRoot
se aplique específicamente al UIViewController
raíz. Luego, el enrutador eliminará todos los UIViewController
representados UIViewController
y la configuración funcionará en cualquier situación.
Quiero mostrar algunos AccountViewController
, si todavía se muestra bien, dentro de cualquier UINavigationController
y que está actualmente en la pantalla en algún lugar (incluso si este UINavigationController
bajo algún UIViewController
modal):
let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(SingleStep(ClassFinder<UINavigationController, Any?>(), NilFactory())) .from(GeneralStep.current()) .assemble()
¿Qué significa NilFactory
en esta configuración? Con esto, le dice al enrutador que si no puede encontrar ningún UINavigationController
en la pantalla, no quiere que lo cree y simplemente no haga nada en este caso. Por cierto, dado que esto es NilFactory
, no puedes usar Action
después de él.
Quiero mostrar algunos AccountViewController
, si aún no se muestran, dentro de cualquier UINavigationController
y que actualmente se encuentra en algún lugar de la pantalla, y si no es así, UINavigationController
y UINavigationController
:
let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.PushToNavigation()) .from(SwitchAssembly<UINavigationController, Any?>() .addCase(expecting: ClassFinder<UINavigationController, Any?>(options: .visible))
Quiero mostrar UITabBarController
con UITabBarController
contienen AccountViewController
y AccountViewController
reemplazándolos con la raíz actual:
let tabScreen = SingleContainerStep( finder: ClassFinder(), factory: CompleteFactoryAssembly(factory: TabBarControllerFactory()) .with(XibFactory<HomeViewController, Any?>(), using: UITabBarController.addTab()) .with(XibFactory<AccountViewController, Any?>(), using: UITabBarController.addTab()) .assemble()) .using(GeneralAction.replaceRoot()) .from(GeneralStep.root()) .assemble()
¿Puedo usar el UIViewControllerTransitioningDelegate
personalizado con la acción GeneralAction.presentModally
:
let transitionController = CustomViewControllerTransitioningDelegate()
Quiero ir a AccountViewController
, donde sea que AccountViewController
el usuario, en otra pestaña o incluso en algún tipo de ventana modal:
let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: NilFactory()) .from(tabScreen) .assemble()
¿Por qué estamos usando NilFactory
? No necesitamos construir un AccountViewController
si no se encuentra. Se construirá en la configuración de tabScreen
. Mírala arriba.
Quiero mostrar ForgotPasswordViewController
, pero, ciertamente, después de LoginViewController
dentro de UINavigationController
:
let loginScreen = StepAssembly( finder: ClassFinder<LoginViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(NavigationControllerStep()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() let forgotPasswordScreen = StepAssembly( finder: ClassFinder<ForgotPasswordViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(loginScreen.expectingContainer()) .assemble()
Puede usar la configuración en el ejemplo para la navegación en ForgotPasswordViewController
y LoginViewController
¿Por qué expectingContainer
en el ejemplo anterior?
Dado que la acción pushToNavigation
requiere la presencia de un UINavigationController
y en la configuración posterior, el método expectingContainer
nos permite evitar un error de compilación al garantizar que tengamos cuidado de que cuando el enrutador llegue a loginScreen
en loginScreen
de loginScreen
, el UINavigationController
esté allí.
¿Qué sucede si en la configuración anterior reemplazo GeneralStep.current
con GeneralStep.root
?
Funcionará, pero dado que le dice al enrutador que desea que comience a construir una cadena desde el UIViewController
raíz, si se abre algún UIViewController
modal encima, el enrutador los ocultará antes de comenzar a construir la cadena.
Mi aplicación tiene un UITabBarController
contiene BagViewController
y BagViewController
como pestañas. Quiero que el usuario pueda cambiar entre ellos usando los iconos en las pestañas como de costumbre. Pero si llamo a la configuración mediante programación (por ejemplo, el usuario hace clic en "Ir a la bolsa" dentro del HomeViewController
), la aplicación no debería cambiar la pestaña, sino mostrar el BagViewController
.
Hay 3 formas de lograr esto en la configuración:
- Configure
StackIteratingFinder
para buscar solo en los visibles usando [.current, .visible] - Utilice
NilFinder
que significa que el enrutador nunca encontrará el BagViewController en las BagViewController
y siempre lo creará. Sin embargo, este enfoque tiene un efecto secundario: si, por ejemplo, un usuario que ya está en BagViewController
se presenta modalmente y, por ejemplo, hace clic en un enlace universal que BagViewController
debería mostrarle, el enrutador no lo encontrará y creará otra instancia y la mostrará encima de ella modalmente. Esto puede no ser lo que quieres. - Cambie un poco de
ClassFinder
para que encuentre solo el BagViewController
muestra BagViewController
e ignore el resto, y ya lo use en la configuración.
struct ModalBagFinder: StackIteratingFinder { func isTarget(_ viewController: BagViewController, with context: Any?) -> Bool { return viewController.presentingViewController != nil } } let screen = StepAssembly( finder: ModalBagFinder(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(NavigationControllerStep()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble()
En lugar de una conclusión
Espero que los métodos de configuración del enrutador se hayan vuelto algo más claros. Como dije, usamos este enfoque en 3 aplicaciones y aún no hemos encontrado una situación en la que no fuera lo suficientemente flexible. La biblioteca, así como la implementación del enrutador que se le proporciona, no utiliza ningún truco objetivo con el tiempo de ejecución y sigue completamente todos los conceptos de Cocoa Touch, solo ayuda a dividir el proceso de composición en pasos y ejecutarlos en la secuencia dada y probado con las versiones de iOS 9 a 12. Además , este enfoque se ajusta a todos los patrones arquitectónicos que implican trabajar con la pila UIViewController
(MVC, MVVM, VIP, RIB, VIPER, etc.)
Estaría encantado de sus comentarios y sugerencias. Especialmente si crees que vale la pena detenerse en algunos aspectos con más detalle. Quizás el concepto de contextos necesita aclaración.