L'injection de dépendances est un modèle assez populaire qui vous permet de configurer le système de manière flexible et de créer correctement les dépendances des composants de ce système les uns sur les autres. Grâce à la saisie, Swift vous permet d'utiliser des frameworks pratiques avec lesquels vous pouvez décrire très brièvement le graphe des dépendances. Aujourd'hui, je veux parler un peu de l'un de ces cadres - DITranquillity
.
Ce didacticiel couvrira les fonctionnalités de bibliothèque suivantes:
- Enregistrement de type
- Déploiement d'initialisation
- Incorporation dans une variable
- Dépendances cycliques des composants
- Utilisation de la bibliothèque avec
UIStoryboard
Description des composants
L'application se composera des principaux composants suivants: ViewController
, Router
, Presenter
, Networking
- ce sont des composants assez communs dans toute application iOS.
ViewController
et Router
seront introduits l'un dans l'autre de manière cyclique.
La préparation
Tout d'abord, créez une application à vue unique dans Xcode, ajoutez DITranquillity à l'aide de CocoaPods . Créez la hiérarchie de fichiers nécessaire, puis ajoutez un deuxième contrôleur à Main.storyboard et connectez-le à l'aide de StoryboardSegue
. Par conséquent, la structure de fichiers suivante doit être obtenue:
Créez des dépendances dans les classes comme suit:
Déclaration des composants protocol Presenter: class { func getCounter(completion: @escaping (Int) -> Void) } class MyPresenter: Presenter { private let networking: Networking init(networking: Networking) { self.networking = networking } func getCounter(completion: @escaping (Int) -> Void) {
protocol Networking: class { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) } class MyNetworking: Networking { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) {
protocol Router: class { func presentNewController() } class MyRouter: Router { unowned let viewController: ViewController init(viewController: ViewController) { self.viewController = viewController } func presentNewController() {
class ViewController: UIViewController { var presenter: Presenter! var router: Router! }
Limitations
Contrairement Ă d'autres classes, le ViewController
n'est pas créé par nous, mais par la bibliothèque UIKit à l'intérieur de l'implémentation UIStoryboard.instantiateViewController
, par conséquent, en utilisant le storyboard, nous ne pouvons pas injecter de dépendances dans les héritiers de l' UIViewController
à l'aide de l'initialiseur. Il en va de même avec les héritiers de UIView
et UITableViewCell
.
Notez que les objets cachés derrière les protocoles sont intégrés dans toutes les classes. C'est l'une des tâches principales de l'implémentation des dépendances - faire des dépendances non pas sur les implémentations, mais sur les interfaces. Cela aidera à l'avenir à fournir différentes implémentations de protocole pour réutiliser ou tester des composants.
Injection de dépendance
Une fois tous les composants du système créés, nous procédons à la connexion des objets entre eux. Dans DITranquillity, le point de départ est DIContainer
, qui ajoute l'enregistrement à l'aide de la méthode container.register(...)
. Pour séparer les dépendances en parties, DIFramework
et DIPart
, qui doivent être implémentés. Pour plus de commodité, nous ne créerons qu'une seule classe ApplicationDependency
, qui implémentera DIFramework
et servira de lieu d'enregistrement pour toutes les dépendances. L'interface DIFramework
vous oblige à implémenter une seule méthode - load(container:)
.
class ApplicationDependency: DIFramework { static func load(container: DIContainer) {
Commençons par l'inscription la plus simple qui n'a pas de dépendances - MyNetworking
container.register(MyNetworking.init)
Cet enregistrement utilise l'implémentation via l'initialiseur. Malgré le fait que le composant lui-même n'a pas de dépendances, l'initialiseur doit être fourni pour indiquer clairement à la bibliothèque comment créer le composant.
De mĂŞme, enregistrez MyPresenter
et MyRouter
.
container.register1(MyPresenter.init) container.register1(MyRouter.init)
Remarque: Notez que ce n'est pas le register
qui est utilisé, mais le register1
. Malheureusement, cela est nécessaire pour indiquer si l'objet a une et une seule dépendance dans l'initialiseur. Autrement dit, s'il existe 0 ou deux dépendances ou plus, il vous suffit d'utiliser le register
. Cette restriction est un bogue de Swift version 4.0 et supérieure.
Il est temps d'enregistrer notre ViewController
. Il injecte des objets non pas via l'initialiseur, mais directement dans la variable, de sorte que la description de l'enregistrement se révélera un peu plus.
container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter)
La syntaxe du formulaire \.presenter
.presenter est SwiftKeyPath, grâce à laquelle il est possible d'implémenter de manière concise les dépendances. Étant donné que Router
et ViewController
cycliquement dépendants l'un de l'autre, vous devez explicitement l'indiquer en utilisant cycle: true
. La bibliothèque elle-même peut résoudre ces dépendances sans indication explicite, mais cette exigence a été introduite afin que la personne lisant le graphique comprenne immédiatement qu'il existe des cycles dans la chaîne de dépendances. Notez également que NOT ViewController.init
, mais ViewController.self
. Cela a été décrit ci-dessus dans la section Limitations .
Il est également nécessaire d'enregistrer l' UIStoryboard
aide d'une méthode spéciale.
container.registerStoryboard(name: "Main")
Nous avons maintenant décrit le graphique de dépendance entier pour un écran. Mais il n'y a pas encore accès à ce graphique. Vous devez créer un DIContainer
qui vous permet d'accéder aux objets qu'il DIContainer
.
static let container: DIContainer = { let container = DIContainer()
- Initialiser le conteneur
- Ajoutez-y une description du graphique.
- Nous vérifions que nous avons tout fait correctement. En cas d'erreur, l'application ne se bloquera pas lors de la résolution des dépendances, mais immédiatement lors de la création d'un graphique
Ensuite, vous devez faire du conteneur le point de départ de l'application. Pour ce faire, AppDelegate
implémentons la méthode didFinishLaunchingWithOptions
dans didFinishLaunchingWithOptions
au lieu de spécifier Main.storyboard
comme point de lancement dans les paramètres du projet.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let storyboard: UIStoryboard = ApplicationDependency.container.resolve() window?.rootViewController = storyboard.instantiateInitialViewController() window?.makeKeyAndVisible() return true }
Lancement
Au premier démarrage, une baisse se produit et la validation échoue pour les raisons suivantes:
- Le conteneur ne trouvera pas les types
Router
, Presenter
, Networking
, car nous n'avons enregistré que des objets. Si nous voulons donner accès non pas aux implémentations, mais aux interfaces, nous devons explicitement spécifier les interfaces - Le conteneur ne comprend pas comment résoudre la dépendance cyclique, car il est nécessaire d'indiquer explicitement quels objets ne doivent pas être recréés à chaque résolution du graphique.
Il est simple de corriger la première erreur - il existe une méthode spéciale qui vous permet de spécifier sous quels protocoles la méthode dans le conteneur est disponible.
container.register(MyNetworking.init) .as(check: Networking.self) {$0}
Décrivant l'enregistrement comme suit, nous disons: l'objet MyNetworking
est accessible via le protocole Networking
. Cela doit être fait pour tous les objets cachés sous les protocoles. {$0}
ajoutons pour la vérification correcte des types par le compilateur.
La deuxième erreur est un peu plus compliquée. Il est nécessaire d'utiliser la soi-disant scope
, qui décrit à quelle fréquence l'objet est créé et combien il vit. Pour chaque enregistrement participant à une dépendance circulaire, vous devez spécifier une scope
égale à objectGraph
. Cela indiquera clairement au conteneur que lors de la résolution, il est nécessaire de réutiliser les mêmes objets créés, plutôt que de créer à chaque fois. Ainsi, il s'avère:
container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter) .lifetime(.objectGraph) container.register1(MyRouter.init) .as(check: Router.self) {$0} .lifetime(.objectGraph)
Après le redémarrage, le conteneur réussit la validation et notre ViewController s'ouvre avec les dépendances créées. Vous pouvez mettre un point d'arrêt dans viewDidLoad
et vous en assurer.
Transition entre écrans
Ensuite, créez deux petites classes, SecondViewController
et SecondPresenter
, ajoutez SecondViewController
au storyboard et créez un Segue
entre elles avec l'identifiant "RouteToSecond"
, qui vous permet d'ouvrir le deuxième contrôleur à partir du premier.
Ajoutez deux enregistrements supplémentaires à notre ApplicationDependency
pour chacune des nouvelles classes:
container.register(SecondViewController.self) .injection(\.secondPresenter) container.register(SecondPresenter.init)
.as
pas nécessaire de spécifier .as
, car nous n'avons pas caché SecondPresenter
derrière le protocole, mais utilisons directement l'implémentation. Ensuite, dans la méthode viewDidAppear
du premier contrĂ´leur, nous appelons performSegue(withIdentifier: "RouteToSecond", sender: self)
, start, le deuxième contrôleur s'ouvre, dans lequel la dépendance secondPresenter
doit être apposée. Comme vous pouvez le voir, le conteneur a vu la création d'un deuxième contrôleur à partir d' UIStoryboard
et a réussi à supprimer les dépendances.
Conclusion
Cette bibliothèque vous permet de travailler facilement avec les dépendances cycliques, les storyboards et utilise pleinement l'inférence de type automatique dans Swift, qui donne une syntaxe très courte et flexible pour décrire le graphique des dépendances.
Les références
Exemple de code complet dans la bibliothèque github
DITranquillity sur github
Article en anglais