Contexte:
Bien entendu, le choix d'une approche architecturale influe sur la mise en œuvre de la navigation et l'organisation du transport de données dans un projet, cependant, la démarche elle-même est constituée d'un certain nombre de circonstances: la composition de l'équipe, le time to market, l'état de la spécification technique, l'évolutivité du projet, etc.
- utilisation obligatoire de MVVM;
- la possibilité d'ajouter rapidement de nouveaux écrans (contrôleurs et leurs modèles de vue) au processus de navigation;
- les changements de logique métier ne devraient pas affecter la navigation;
- les changements de navigation ne devraient pas affecter la logique métier;
- la possibilité de réutiliser rapidement les écrans sans apporter de corrections à la navigation;
- la possibilité de se faire rapidement une idée des écrans existants;
- la possibilité de se faire rapidement une idée des dépendances dans le projet;
- n'augmentez pas le seuil pour que les développeurs entrent dans le projet.
Arriver au point
Il est à noter que la solution finale ne s'est pas formée en un jour, elle n'est pas sans inconvénients et convient mieux aux projets de petite et moyenne envergure. Pour plus de clarté, le projet de test peut être consulté ici: github.com/ArturRuZ/NavigationDemo
1. Pour pouvoir se faire une idée rapide des écrans existants, il a été décidé de créer une énumération avec le nom sans ambiguïté ControllersList.
enum ControllersList {
case textInputScreen
case textConfirmationScreen
}
2. Pour un certain nombre de raisons, le projet ne souhaitait pas utiliser de solutions tierces pour DI, et je voulais obtenir DI, notamment avec la possibilité de visualiser rapidement les dépendances dans le projet, il a donc été décidé d'utiliser Assembly pour chaque écran séparé (fermé par le protocole Assembly) et RootAssembly comme portée générale.
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. Pour transférer des données entre les écrans (là où c'est vraiment nécessaire) ControllersList transformé en une énumération avec des valeurs associées:
enum ControllersList {
case textInputScreen
case textConfirmationScreen(text: String)
}
4. Pour que la logique métier n'affecte pas la navigation, ni la navigation sur la logique métier, ainsi que pour une réutilisation rapide des écrans, il était nécessaire de déplacer la navigation vers une couche séparée. Voici comment le coordinateur et le protocole de coordination sont apparus:
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)
}
}
Il est important de noter ici que le protocole peut décrire plus de méthodes, incl. comme le coordinateur, il peut mettre en œuvre différents protocoles, en fonction des besoins.
5. Avec tout cela, je voulais aussi limiter l'ensemble des actions que le développeur devait effectuer en ajoutant un nouvel écran à l'application. Pour le moment, il était nécessaire de se rappeler que quelque part, vous devez enregistrer les dépendances, et il est possible de faire d'autres actions pour que la navigation fonctionne.
6. Je ne voulais pas du tout créer de routeurs et de coordinateurs supplémentaires. De plus, créer une logique de navigation supplémentaire pourrait compliquer considérablement à la fois la perception de la navigation et la réutilisation des écrans. Tout cela a conduit à une chaîne de changements qui ressemblait finalement à ceci:
//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
}
}
Désormais, lors de la création d'un nouvel écran, le développeur devait simplement apporter des modifications à la ControllersList, puis le compilateur lui-même montrait où il était nécessaire d'apporter des modifications. L'ajout de nouveaux écrans à la ControllersList n'affectait en rien le schéma de navigation actuel et la logique de gestion des dépendances était facile à suivre. En outre, en utilisant ControllersList, vous pouvez facilement trouver tous les points d'entrée dans un écran particulier, et il est devenu facile de réutiliser les écrans.
Conclusion
Cet exemple est une implémentation simplifiée de l'idée et ne couvre pas tous les cas d'utilisation; néanmoins, l'approche elle-même s'est révélée assez flexible et adaptative.
Les inconvénients de cette approche sont les suivants:
- , , . ControllersList NavigationEvents, , ;
- , ;
- , , . , .
La plupart des articles sur la navigation et le transfert de données dans les applications IOS affectent soit l'utilisation des coordinateurs et des routeurs (pour chacun ou un groupe d'écrans), soit la navigation à travers segue, singleton, etc., mais aucune de ces options ne me convenait pour l'une ou l'autre. les raisons.
Peut-être que cette approche vous conviendra pour résoudre des problèmes, merci pour votre temps!