Uso de valores asociados de Enum + al navegar y transferir datos entre pantallas en aplicaciones IOS

En esta publicación, me gustaría abordar la antigua cuestión de organizar la navegación y la transferencia de datos entre pantallas en aplicaciones IOS. En primer lugar, me gustaría transmitir el concepto de mi enfoque y no convencerlo de que lo use como una píldora mágica. Aquí no consideraremos varios enfoques arquitectónicos o la posibilidad de usar UlStoryboard con segues, en general describiré otra forma posible de lograr lo que quieres con sus pros y contras. ¡Vamos a empezar!



Antecedentes:



Por supuesto, la elección de un enfoque arquitectónico afecta la implementación de la navegación y la organización del transporte de datos en el proyecto, sin embargo, el enfoque en sí se compone de una serie de circunstancias: composición del equipo, tiempo de comercialización, estado de la especificación técnica, escalabilidad del proyecto y muchos otros, los factores determinantes para mí fueron:



  • uso obligatorio de MVVM;
  • la capacidad de agregar rápidamente nuevas pantallas (controladores y sus modelos de vista) al proceso de navegación;
  • los cambios en la lógica empresarial no deberían afectar la navegación;
  • los cambios en la navegación no deberían afectar la lógica empresarial;
  • la capacidad de reutilizar pantallas rápidamente sin realizar correcciones en la navegación;
  • la capacidad de tener una idea rápida de las pantallas existentes;
  • la capacidad de tener una idea rápida de las dependencias en el proyecto;
  • no eleve el umbral para que los desarrolladores ingresen al proyecto.




Llegar al punto



Cabe señalar que la solución final no se formó en un día, no está exenta de inconvenientes y es más adecuada para proyectos pequeños y medianos. Para mayor claridad, el proyecto de prueba se puede ver aquí: github.com/ArturRuZ/NavigationDemo



1. Para poder tener una idea rápida de las pantallas existentes, se decidió crear una enumeración con el nombre inequívoco ControllersList.



enum ControllersList {
   case textInputScreen
   case textConfirmationScreen
}


2. Por varias razones, el proyecto no quería usar soluciones de terceros para DI, y yo quería obtener DI, incluida la capacidad de ver rápidamente las dependencias en el proyecto, por lo que se decidió usar Assembly para cada pantalla separada (cerrada por el protocolo de Assembly) y RootAssembly como alcance general.




protocol Assembly {
   func build() -> UIViewController
}

final class TextInputAssembly: Assembly {
   func build() -> UIViewController {
      let viewModel = TextInputViewModel()
      return TextInputViewController(viewModel: viewModel)
   }
}

final class TextConfirmationAssembly: Assembly {
   private let text: String
   
   init(text: String) {
      self.text = text
   }
   
   func build() -> UIViewController {
      let viewModel = TextConfirmationViewModel(text: text)
      return TextConfirmationViewController(viewModel: viewModel)
   }
}


3. Para transferir datos entre pantallas (donde sea realmente necesario) ControllersList convertido en una enumeración con Associated Values:



enum ControllersList {
   case textInputScreen
   case textConfirmationScreen(text: String)
}


4. Para que la lógica empresarial no afecte la navegación, ni la navegación en la lógica empresarial, así como para reutilizar rápidamente las pantallas, fue necesario mover la navegación a una capa separada. Así surgió la Coordinadora y el protocolo de Coordinación:




protocol Coordination {
   func show(view: ControllersList, firstPosition: Bool)
   func popFromCurrentController()
}

final class Coordinator {
   
   private var navigationController = UINavigationController()
   private var factory: ControllerBuilder?
   
   private func navigateWithFirstPositionInStack(to: UIViewController) {
      navigationController.viewControllers = [to]
   }
   private func navigate(to: UIViewController) {
      navigationController.pushViewController(to, animated: true)
   }
}

extension Coordinator: Coordination {
   func popFromCurrentController() {
      navigationController.popViewController(animated: true)
   }
   func show(view: ControllersList, firstPosition: Bool) {
      guard let controller = factory?.buildController(for: view) else { return }
                 firstPosition ?  navigateWithFirstPositionInStack(to: controller) : navigate(to: controller)
   }
}



Es importante señalar aquí que el protocolo puede describir más métodos, incl. al igual que el Coordinador, puede implementar diferentes protocolos, según las necesidades.



5. Con todo esto, también quería limitar el conjunto de acciones que el desarrollador tenía que realizar agregando una nueva pantalla a la aplicación. Por el momento, era necesario recordar que en algún lugar es necesario registrar dependencias y es posible realizar otras acciones para que la navegación funcione.



6. No quería crear enrutadores y coordinadores adicionales en absoluto. Además, crear una lógica adicional para la navegación podría complicar significativamente tanto la percepción de la navegación como la reutilización de las pantallas. Todo esto llevó a una cadena de cambios que finalmente se veía así:




//MARK - Dependences with controllers associations
fileprivate extension ControllersList {
   typealias scope = AssemblyServices
  
   var assembly: Assembly {
      switch self {
      case .textInputScreen:
         return TextInputAssembly(coordinator: scope.coordinator)
      case .textConfirmationScreen(let text):
         return TextConfirmationAssembly(coordinator: scope.coordinator, text: text)
      }
   }
}

//MARK - Services all time in memory
fileprivate enum AssemblyServices {
   static let coordinator: oordinationDependencesRegstration = Coordinator()
   static let controllerFactory: ControllerBuilderDependencesRegistration = ControllerFacotry()
}

//MARL: - RootAssembly Implementation
final class  RootAssembly {
   fileprivate typealias scope = AssemblyServices
  
   private func registerPropertyDependences() {
//     this place for propery dependences
   }
}


// MARK: - AssemblyDataSource implementation
extension RootAssembly: AssemblyDataSource {
   func getAssembly(key: ControllersList) -> Assembly? {
      return key.assembly
   }
}


Ahora, al crear una nueva pantalla, el desarrollador solo tenía que realizar cambios en ControllersList, y luego el propio compilador mostraba dónde era necesario realizar cambios. Agregar nuevas pantallas a ControllersList no afectó el esquema de navegación actual de ninguna manera, y la lógica de administración de dependencias fue fácil de seguir. Además, con ControllersList, puede encontrar fácilmente todos los puntos de entrada en una pantalla en particular, y se volvió fácil reutilizar las pantallas.



Conclusión



Este ejemplo es una implementación simplificada de la idea y no cubre todos los casos de uso; sin embargo, el enfoque en sí ha demostrado ser bastante flexible y adaptable.



Las desventajas de este enfoque son las siguientes:



  • , , . ControllersList NavigationEvents, , ;
  • , ;
  • , , . , .


La mayoría de las publicaciones sobre navegación y transferencia de datos en aplicaciones IOS afectan el uso de coordinadores y enrutadores (para cada una o un grupo de pantallas), o la navegación a través de segue, singleton, etc., pero ninguna de estas opciones me convenía para una u otra. razones.



Quizás este enfoque le convenga para resolver problemas, ¡gracias por su tiempo!



All Articles