From af386df61020ee989439eb220f784cf0c83fd28c Mon Sep 17 00:00:00 2001 From: Cassius Pacheco Date: Wed, 11 Mar 2020 22:53:42 +1100 Subject: [PATCH] Add DI framework as dependency Use the DependencyContainer framework to inject containers in the routers which will be responsible for instantiating the whole flow of the app. The Graph's container should register and resolve all dependencies of the app. In order to do it the container must be passed down to every Router. No other classes besides Routers should have access to it. Routers are the only responsible for instantiating new screens and they are in charge of injecting all necessary dependencies into them. --- README.md | 2 +- RoutingExample.xcodeproj/project.pbxproj | 53 +++++++++++++++---- .../contents.xcworkspacedata | 2 +- .../xcshareddata/swiftpm/Package.resolved | 16 ++++++ .../ForgottenPasswordViewController.swift | 4 +- .../ForgottenPasswordViewModel.swift | 6 ++- .../Authentication/LoginViewController.swift | 4 +- .../Authentication/LoginViewModel.swift | 8 ++- .../Authentication/SignUpViewController.swift | 4 +- .../Authentication/SignUpViewModel.swift | 7 ++- RoutingExample/DependencyGraph.swift | 48 +++++++++++++++++ .../PopUps/PopUpViewController.swift | 4 +- RoutingExample/PopUps/PopUpViewModel.swift | 8 ++- .../Product/ProductViewController.swift | 4 +- RoutingExample/Product/ProductViewModel.swift | 9 +++- RoutingExample/Routing/Router.swift | 3 +- .../Routing/Routers/DefaultRouter.swift | 5 +- .../Routes/ForgottenPasswordRoute.swift | 18 ++++--- .../Routing/Routes/LoginRoute.swift | 17 +++--- .../Routing/Routes/PopUpRoute.swift | 17 +++--- .../Routing/Routes/ProductRoute.swift | 18 ++++--- .../Routing/Routes/SignUpRoute.swift | 17 +++--- .../Routing/Routes/Tabs/ShopTabRoute.swift | 15 +++++- .../Routes/Tabs/WishlistTabRoute.swift | 15 +++++- RoutingExample/SceneDelegate.swift | 18 ++++++- RoutingExample/Tabs/ShopViewController.swift | 4 +- RoutingExample/Tabs/ShopViewModel.swift | 6 ++- .../Tabs/WishlistViewController.swift | 4 +- RoutingExample/Tabs/WishlistViewModel.swift | 7 ++- 29 files changed, 271 insertions(+), 72 deletions(-) create mode 100644 RoutingExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 RoutingExample/DependencyGraph.swift diff --git a/README.md b/README.md index 5a449f6..287257b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Clean, Simple and Composable Routing for iOS Apps -This is the [first part](https://cassiuspacheco.com/clean-simple-and-composable-routing-for-ios-apps-ck7qv6kgo0063zns1gdpgwrue) of a series of blog posts about [Clean, Simple and Composable Routing for iOS Apps](https://hashnode.com/series/clean-simple-and-composable-routing-for-ios-apps-ck7vm42k401n4zis1wu4ar2od). +This is the [second part](https://cassiuspacheco.com/applying-dependency-injection-to-composable-routing-for-ios-apps-ck7v10xaz01h9zis1oksoe7h0) of a series of blog posts about [Clean, Simple and Composable Routing for iOS Apps](https://hashnode.com/series/clean-simple-and-composable-routing-for-ios-apps-ck7vm42k401n4zis1wu4ar2od). ## App's Flow Diagram diff --git a/RoutingExample.xcodeproj/project.pbxproj b/RoutingExample.xcodeproj/project.pbxproj index e603b00..0b2ea45 100644 --- a/RoutingExample.xcodeproj/project.pbxproj +++ b/RoutingExample.xcodeproj/project.pbxproj @@ -3,12 +3,14 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ - 6A10678A241CDA3B00844681 /* WishlistTabRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A106788241CDA3B00844681 /* WishlistTabRoute.swift */; }; - 6A10678B241CDA3B00844681 /* ShopTabRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A106789241CDA3B00844681 /* ShopTabRoute.swift */; }; + 6A46A76E2419052200ED7426 /* ShopTabRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A46A76D2419052200ED7426 /* ShopTabRoute.swift */; }; + 6A46A770241905EB00ED7426 /* WishlistTabRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A46A76F241905EB00ED7426 /* WishlistTabRoute.swift */; }; + 6A46A773241908F400ED7426 /* DependencyGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A46A772241908F400ED7426 /* DependencyGraph.swift */; }; + 6A51EA05241DB801004FA2D0 /* DependencyContainer in Frameworks */ = {isa = PBXBuildFile; productRef = 6A51EA04241DB801004FA2D0 /* DependencyContainer */; }; 6A61168224136AE30099C25F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A61168124136AE30099C25F /* AppDelegate.swift */; }; 6A61168424136AE30099C25F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A61168324136AE30099C25F /* SceneDelegate.swift */; }; 6A61168624136AE30099C25F /* ShopViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A61168524136AE30099C25F /* ShopViewController.swift */; }; @@ -102,8 +104,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 6A106788241CDA3B00844681 /* WishlistTabRoute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WishlistTabRoute.swift; sourceTree = ""; }; - 6A106789241CDA3B00844681 /* ShopTabRoute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShopTabRoute.swift; sourceTree = ""; }; + 6A46A76D2419052200ED7426 /* ShopTabRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopTabRoute.swift; sourceTree = ""; }; + 6A46A76F241905EB00ED7426 /* WishlistTabRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishlistTabRoute.swift; sourceTree = ""; }; + 6A46A772241908F400ED7426 /* DependencyGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyGraph.swift; sourceTree = ""; }; 6A61167E24136AE30099C25F /* RoutingExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RoutingExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6A61168124136AE30099C25F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 6A61168324136AE30099C25F /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -165,6 +168,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6A51EA05241DB801004FA2D0 /* DependencyContainer in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -193,11 +197,11 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 6A106787241CDA3B00844681 /* Tabs */ = { + 6A46A7712419085100ED7426 /* Tabs */ = { isa = PBXGroup; children = ( - 6A106788241CDA3B00844681 /* WishlistTabRoute.swift */, - 6A106789241CDA3B00844681 /* ShopTabRoute.swift */, + 6A46A76D2419052200ED7426 /* ShopTabRoute.swift */, + 6A46A76F241905EB00ED7426 /* WishlistTabRoute.swift */, ); path = Tabs; sourceTree = ""; @@ -230,6 +234,7 @@ children = ( 6A61169124136AE50099C25F /* Info.plist */, 6A61168124136AE30099C25F /* AppDelegate.swift */, + 6A46A772241908F400ED7426 /* DependencyGraph.swift */, 6A61168324136AE30099C25F /* SceneDelegate.swift */, 6A61168C24136AE50099C25F /* Assets.xcassets */, 6A61168E24136AE50099C25F /* LaunchScreen.storyboard */, @@ -338,7 +343,7 @@ 6AD8D5782413B81C00889F2D /* ProductRoute.swift */, 6AD8D5702413853A00889F2D /* SignUpRoute.swift */, 6AD8D5982413F7AA00889F2D /* SiriRoute.swift */, - 6A106787241CDA3B00844681 /* Tabs */, + 6A46A7712419085100ED7426 /* Tabs */, ); path = Routes; sourceTree = ""; @@ -410,6 +415,9 @@ 6AD8D5B624146A0F00889F2D /* PBXTargetDependency */, ); name = RoutingExample; + packageProductDependencies = ( + 6A51EA04241DB801004FA2D0 /* DependencyContainer */, + ); productName = RoutingExample; productReference = 6A61167E24136AE30099C25F /* RoutingExample.app */; productType = "com.apple.product-type.application"; @@ -500,6 +508,9 @@ Base, ); mainGroup = 6A61167524136AE30099C25F; + packageReferences = ( + 6A51EA03241DB801004FA2D0 /* XCRemoteSwiftPackageReference "DependencyContainer" */, + ); productRefGroup = 6A61167F24136AE30099C25F /* Products */; projectDirPath = ""; projectRoot = ""; @@ -560,6 +571,7 @@ 6AD8D58A2413D8C800889F2D /* ProductViewModel.swift in Sources */, 6A61168624136AE30099C25F /* ShopViewController.swift in Sources */, 6AD8D5722413853B00889F2D /* SignUpRoute.swift in Sources */, + 6A46A773241908F400ED7426 /* DependencyGraph.swift in Sources */, 6AD8D57C2413D33D00889F2D /* DefaultRouter.swift in Sources */, 6AD8D5732413853B00889F2D /* LoginRoute.swift in Sources */, 6AD8D56B2413848700889F2D /* ModalTransition.swift in Sources */, @@ -571,9 +583,10 @@ 6AD8D56C2413848700889F2D /* Transition.swift in Sources */, 6A6116A624136C5C0099C25F /* MainTabBarController.swift in Sources */, 6A6116BD241372270099C25F /* LayoutHelper.swift in Sources */, + 6A46A770241905EB00ED7426 /* WishlistTabRoute.swift in Sources */, 6AD8D5862413D73B00889F2D /* WishlistViewModel.swift in Sources */, + 6A46A76E2419052200ED7426 /* ShopTabRoute.swift in Sources */, 6AD8D56D2413848700889F2D /* PushTransition.swift in Sources */, - 6A10678A241CDA3B00844681 /* WishlistTabRoute.swift in Sources */, 6AD8D5992413F7AA00889F2D /* SiriRoute.swift in Sources */, 6AD8D5C82414BBAE00889F2D /* UIViewController.swift in Sources */, 6AD8D5752413B16300889F2D /* ForgottenPasswordRoute.swift in Sources */, @@ -589,7 +602,6 @@ 6AD8D5882413D83900889F2D /* PopUpViewModel.swift in Sources */, 6A61168424136AE30099C25F /* SceneDelegate.swift in Sources */, 6AD8D5772413B24000889F2D /* PopUpRoute.swift in Sources */, - 6A10678B241CDA3B00844681 /* ShopTabRoute.swift in Sources */, 6A6116BF241372C70099C25F /* DefaultButton.swift in Sources */, 6AD8D5792413B81C00889F2D /* ProductRoute.swift in Sources */, ); @@ -983,6 +995,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 6A51EA03241DB801004FA2D0 /* XCRemoteSwiftPackageReference "DependencyContainer" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/CassiusPacheco/DependencyContainer"; + requirement = { + kind = exactVersion; + version = 0.1.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 6A51EA04241DB801004FA2D0 /* DependencyContainer */ = { + isa = XCSwiftPackageProductDependency; + package = 6A51EA03241DB801004FA2D0 /* XCRemoteSwiftPackageReference "DependencyContainer" */; + productName = DependencyContainer; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 6A61167624136AE30099C25F /* Project object */; } diff --git a/RoutingExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/RoutingExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 80af39e..919434a 100644 --- a/RoutingExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/RoutingExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/RoutingExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/RoutingExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..3f06eb1 --- /dev/null +++ b/RoutingExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "DependencyContainer", + "repositoryURL": "https://github.com/CassiusPacheco/DependencyContainer", + "state": { + "branch": null, + "revision": "15049d95d20dabca6337c4c63c1ff98f3b776e07", + "version": "0.1.0" + } + } + ] + }, + "version": 1 +} diff --git a/RoutingExample/Authentication/ForgottenPasswordViewController.swift b/RoutingExample/Authentication/ForgottenPasswordViewController.swift index c571cd7..30d8732 100644 --- a/RoutingExample/Authentication/ForgottenPasswordViewController.swift +++ b/RoutingExample/Authentication/ForgottenPasswordViewController.swift @@ -9,9 +9,9 @@ import UIKit final class ForgottenPasswordViewController: UIViewController { - private let viewModel: ForgottenPasswordViewModel + private let viewModel: ForgottenPasswordViewModelInterface - init(viewModel: ForgottenPasswordViewModel) { + init(viewModel: ForgottenPasswordViewModelInterface) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } diff --git a/RoutingExample/Authentication/ForgottenPasswordViewModel.swift b/RoutingExample/Authentication/ForgottenPasswordViewModel.swift index 9c4fd03..102043c 100644 --- a/RoutingExample/Authentication/ForgottenPasswordViewModel.swift +++ b/RoutingExample/Authentication/ForgottenPasswordViewModel.swift @@ -8,7 +8,11 @@ import Foundation -final class ForgottenPasswordViewModel { +protocol ForgottenPasswordViewModelInterface { + func resetPasswordButtonTouchUpInside() +} + +final class ForgottenPasswordViewModel: ForgottenPasswordViewModelInterface { typealias Routes = PopUpRoute private let router: Routes diff --git a/RoutingExample/Authentication/LoginViewController.swift b/RoutingExample/Authentication/LoginViewController.swift index f335cd1..d0d883b 100644 --- a/RoutingExample/Authentication/LoginViewController.swift +++ b/RoutingExample/Authentication/LoginViewController.swift @@ -9,9 +9,9 @@ import UIKit final class LoginViewController: UIViewController { - private let viewModel: LoginViewModel + private let viewModel: LoginViewModelInterface - init(viewModel: LoginViewModel) { + init(viewModel: LoginViewModelInterface) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } diff --git a/RoutingExample/Authentication/LoginViewModel.swift b/RoutingExample/Authentication/LoginViewModel.swift index a4ff5a7..385ef38 100644 --- a/RoutingExample/Authentication/LoginViewModel.swift +++ b/RoutingExample/Authentication/LoginViewModel.swift @@ -8,7 +8,13 @@ import Foundation -final class LoginViewModel { +protocol LoginViewModelInterface { + func dismissButtonTouchUpInside() + func forgottenPasswordButtonTouchUpInside() + func signUpButtonTouchUpInside() +} + +final class LoginViewModel: LoginViewModelInterface { typealias Routes = LoginRoute & SignUpRoute & ForgottenPasswordRoute & Closable private var router: Routes diff --git a/RoutingExample/Authentication/SignUpViewController.swift b/RoutingExample/Authentication/SignUpViewController.swift index 12f3b13..69ed4f0 100644 --- a/RoutingExample/Authentication/SignUpViewController.swift +++ b/RoutingExample/Authentication/SignUpViewController.swift @@ -9,9 +9,9 @@ import UIKit final class SignUpViewController: UIViewController { - private let viewModel: SignUpViewModel + private let viewModel: SignUpViewModelInterface - init(viewModel: SignUpViewModel) { + init(viewModel: SignUpViewModelInterface) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } diff --git a/RoutingExample/Authentication/SignUpViewModel.swift b/RoutingExample/Authentication/SignUpViewModel.swift index f62f9f6..90003c8 100644 --- a/RoutingExample/Authentication/SignUpViewModel.swift +++ b/RoutingExample/Authentication/SignUpViewModel.swift @@ -8,7 +8,12 @@ import Foundation -final class SignUpViewModel { +protocol SignUpViewModelInterface { + func forgottenPasswordButtonTouchUpInside() + func dismissButtonTouchUpInside() +} + +final class SignUpViewModel: SignUpViewModelInterface { typealias Routes = ForgottenPasswordRoute & Dismissable private var router: Routes diff --git a/RoutingExample/DependencyGraph.swift b/RoutingExample/DependencyGraph.swift new file mode 100644 index 0000000..fe29313 --- /dev/null +++ b/RoutingExample/DependencyGraph.swift @@ -0,0 +1,48 @@ +// +// DependencyGraph.swift +// RoutingExample +// +// Created by Cassius Pacheco on 11/3/20. +// Copyright © 2020 Cassius Pacheco. All rights reserved. +// + +import Foundation +import DependencyContainer + +final class DependencyGraph { + let container = DependencyContainer() + + func registerDependencies() { + setupViewModels() + } + + private func setupViewModels() { + container.register(LoginViewModelInterface.self) { (_, routes: LoginViewModel.Routes) in + return LoginViewModel(router: routes) + } + + container.register(ForgottenPasswordViewModelInterface.self) { (_, routes: ForgottenPasswordViewModel.Routes) in + return ForgottenPasswordViewModel(router: routes) + } + + container.register(SignUpViewModelInterface.self) { (_, routes: SignUpViewModel.Routes) in + return SignUpViewModel(router: routes) + } + + container.register(PopUpViewModelInterface.self) { (_, message: String, routes: PopUpViewModel.Routes) in + return PopUpViewModel(message: message, router: routes) + } + + container.register(ProductViewModelInterface.self) { (_, routes: ProductViewModel.Routes) in + return ProductViewModel(router: routes) + } + + container.register(ShopViewModelInterface.self) { (_, routes: ShopViewModel.Routes) in + return ShopViewModel(router: routes) + } + + container.register(WishlistViewModelInterface.self) { (_, routes: WishlistViewModel.Routes) in + return WishlistViewModel(router: routes) + } + } +} diff --git a/RoutingExample/PopUps/PopUpViewController.swift b/RoutingExample/PopUps/PopUpViewController.swift index bb659db..3dc4fcd 100644 --- a/RoutingExample/PopUps/PopUpViewController.swift +++ b/RoutingExample/PopUps/PopUpViewController.swift @@ -9,9 +9,9 @@ import UIKit final class PopUpViewController: UIViewController { - private let viewModel: PopUpViewModel + private let viewModel: PopUpViewModelInterface - init(viewModel: PopUpViewModel) { + init(viewModel: PopUpViewModelInterface) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } diff --git a/RoutingExample/PopUps/PopUpViewModel.swift b/RoutingExample/PopUps/PopUpViewModel.swift index 988be35..77ede18 100644 --- a/RoutingExample/PopUps/PopUpViewModel.swift +++ b/RoutingExample/PopUps/PopUpViewModel.swift @@ -8,7 +8,13 @@ import Foundation -final class PopUpViewModel { +protocol PopUpViewModelInterface { + var message: String { get } + + func dismissButtonTouchUpInside() +} + +final class PopUpViewModel: PopUpViewModelInterface { typealias Routes = Closable private let router: Routes let message: String diff --git a/RoutingExample/Product/ProductViewController.swift b/RoutingExample/Product/ProductViewController.swift index 893098a..fe8c883 100644 --- a/RoutingExample/Product/ProductViewController.swift +++ b/RoutingExample/Product/ProductViewController.swift @@ -10,9 +10,9 @@ import UIKit import IntentsUI final class ProductViewController: UIViewController { - private let viewModel: ProductViewModel + private let viewModel: ProductViewModelInterface - init(viewModel: ProductViewModel) { + init(viewModel: ProductViewModelInterface) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } diff --git a/RoutingExample/Product/ProductViewModel.swift b/RoutingExample/Product/ProductViewModel.swift index 75112dc..e587e20 100644 --- a/RoutingExample/Product/ProductViewModel.swift +++ b/RoutingExample/Product/ProductViewModel.swift @@ -9,7 +9,14 @@ import Foundation import IntentsUI -final class ProductViewModel { +protocol ProductViewModelInterface { + var siriButtonDelegate: INUIAddVoiceShortcutButtonDelegate { get } + + func productButtonTouchUpInside() + func wishlistButtonTouchUpInside() +} + +final class ProductViewModel: ProductViewModelInterface { typealias Routes = ProductRoute & PopUpRoute & SiriRoute private let router: Routes diff --git a/RoutingExample/Routing/Router.swift b/RoutingExample/Routing/Router.swift index ce5c78e..5b45701 100644 --- a/RoutingExample/Routing/Router.swift +++ b/RoutingExample/Routing/Router.swift @@ -7,6 +7,7 @@ // import UIKit +import DependencyContainer protocol Closable: class { /// Closes the Router's root view controller using the transition used to show it. @@ -50,5 +51,5 @@ protocol Router: Routable { // Dependency Container example: // https://github.com/CassiusPacheco/DependencyContainer // - // var container: DependencyContainer { get } + var container: DependencyContainer { get } } diff --git a/RoutingExample/Routing/Routers/DefaultRouter.swift b/RoutingExample/Routing/Routers/DefaultRouter.swift index f70fd2e..6268b0c 100644 --- a/RoutingExample/Routing/Routers/DefaultRouter.swift +++ b/RoutingExample/Routing/Routers/DefaultRouter.swift @@ -7,13 +7,16 @@ // import UIKit +import DependencyContainer class DefaultRouter: NSObject, Router, Closable, Dismissable { private let rootTransition: Transition weak var root: UIViewController? + var container: DependencyContainer - init(rootTransition: Transition) { + init(rootTransition: Transition, container: DependencyContainer) { self.rootTransition = rootTransition + self.container = container } deinit { diff --git a/RoutingExample/Routing/Routes/ForgottenPasswordRoute.swift b/RoutingExample/Routing/Routes/ForgottenPasswordRoute.swift index 974fc97..9c8df55 100644 --- a/RoutingExample/Routing/Routes/ForgottenPasswordRoute.swift +++ b/RoutingExample/Routing/Routes/ForgottenPasswordRoute.swift @@ -7,6 +7,7 @@ // import Foundation +import DependencyContainer protocol ForgottenPasswordRoute { func openForgottenPassword() @@ -17,13 +18,18 @@ extension ForgottenPasswordRoute where Self: Router { // internally by instances that conform to Router, like DefaultRouter, // DeeplinkRouter and others. func openForgottenPassword(with transition: Transition) { - // If the `Router` makes use of a DI container it can resolve - // the dependencies in a clean and testable way by doing something like: - // - // let viewController = container.resolve(ForgottenPasswordViewController.self, argument: router) + // Passing the current Router's container into the newly created Router below + let router = DefaultRouter(rootTransition: transition, container: container) - let router = DefaultRouter(rootTransition: transition) - let viewModel = ForgottenPasswordViewModel(router: router) + // The ForgottenPasswordViewModelInterface was registered expecting an argument of the + // type ForgottenPasswordViewModel.Routes. Even though DefaultRouter conforms to it, + // ultimately it is of a different type, so we need to erase its type. + let routes = router as ForgottenPasswordViewModel.Routes + + // Resolve the dependency by returning an instance that conforms to + // ForgottenPasswordViewModelInterface. That may be a real or mock instance. + // This is registered in the DependencyGraph.swift. + let viewModel = container.resolve(ForgottenPasswordViewModelInterface.self, argument: routes) let viewController = ForgottenPasswordViewController(viewModel: viewModel) router.root = viewController diff --git a/RoutingExample/Routing/Routes/LoginRoute.swift b/RoutingExample/Routing/Routes/LoginRoute.swift index 1930add..9219563 100644 --- a/RoutingExample/Routing/Routes/LoginRoute.swift +++ b/RoutingExample/Routing/Routes/LoginRoute.swift @@ -17,13 +17,18 @@ extension LoginRoute where Self: Router { // internally by instances that conform to Router, like DefaultRouter, // DeeplinkRouter and others. func openLogin(with transition: Transition) { - // If the `Router` makes use of a DI container it can resolve - // the dependencies in a clean and testable way by doing something like: - // - // let viewController = container.resolve(LoginViewController.self, argument: router) + // Passing the current Router's container into the newly created Router below + let router = DefaultRouter(rootTransition: transition, container: container) - let router = DefaultRouter(rootTransition: transition) - let viewModel = LoginViewModel(router: router) + // The LoginViewModelInterface was registered expecting an argument of the + // type LoginViewModel.Routes. Even though DefaultRouter conforms to it, + // ultimately it is of a different type, so we need to erase its type. + let routes = router as LoginViewModel.Routes + + // Resolve the dependency by returning an instance that conforms to + // LoginViewModelInterface. That may be a real or mock instance. + // This is registered in the DependencyGraph.swift. + let viewModel = container.resolve(LoginViewModelInterface.self, argument: routes) let viewController = LoginViewController(viewModel: viewModel) let navigationController = UINavigationController(rootViewController: viewController) router.root = viewController diff --git a/RoutingExample/Routing/Routes/PopUpRoute.swift b/RoutingExample/Routing/Routes/PopUpRoute.swift index e0c1a82..69d74b7 100644 --- a/RoutingExample/Routing/Routes/PopUpRoute.swift +++ b/RoutingExample/Routing/Routes/PopUpRoute.swift @@ -17,13 +17,18 @@ extension PopUpRoute where Self: Router { // internally by instances that conform to Router, like DefaultRouter, // DeeplinkRouter and others. func openPopUp(withMessage message: String, transition: Transition) { - // If the `Router` makes use of a DI container it can resolve - // the dependencies in a clean and testable way by doing something like: - // - // let viewController = container.resolve(PopUpViewController.self, argument: router) + // Passing the current Router's container into the newly created Router below + let router = DefaultRouter(rootTransition: transition, container: container) - let router = DefaultRouter(rootTransition: transition) - let viewModel = PopUpViewModel(message: message, router: router) + // The PopUpViewModelInterface was registered expecting an argument of the + // type PopUpViewModel.Routes. Even though DefaultRouter conforms to it, + // ultimately it is of a different type, so we need to erase its type. + let routes = router as PopUpViewModel.Routes + + // Resolve the dependency by returning an instance that conforms to + // PopUpViewModelInterface. That may be a real or mock instance. + // This is registered in the DependencyGraph.swift. + let viewModel = container.resolve(PopUpViewModelInterface.self, arguments: message, routes) let viewController = PopUpViewController(viewModel: viewModel) router.root = viewController diff --git a/RoutingExample/Routing/Routes/ProductRoute.swift b/RoutingExample/Routing/Routes/ProductRoute.swift index 41a769f..0202c7e 100644 --- a/RoutingExample/Routing/Routes/ProductRoute.swift +++ b/RoutingExample/Routing/Routes/ProductRoute.swift @@ -7,6 +7,7 @@ // import Foundation +import DependencyContainer protocol ProductRoute { func openProduct() @@ -17,13 +18,18 @@ extension ProductRoute where Self: Router { // internally by instances that conform to Router, like DefaultRouter, // DeeplinkRouter and others. func openProduct(with transition: Transition) { - // If the `Router` makes use of a DI container it can resolve - // the dependencies in a clean and testable way by doing something like: - // - // let viewController = container.resolve(ProductViewController.self, argument: router) + // Passing the current Router's container into the newly created Router below + let router = SiriRouter(rootTransition: transition, container: container) - let router = SiriRouter(rootTransition: transition) - let viewModel = ProductViewModel(router: router) + // The ProductViewModelInterface was registered expecting an argument of the + // type ProductViewModel.Routes. Even though DefaultRouter conforms to it, + // ultimately it is of a different type, so we need to erase its type. + let routes = router as ProductViewModel.Routes + + // Resolve the dependency by returning an instance that conforms to + // ProductViewModelInterface. That may be a real or mock instance. + // This is registered in the DependencyGraph.swift. + let viewModel = container.resolve(ProductViewModelInterface.self, argument: routes) let viewController = ProductViewController(viewModel: viewModel) router.root = viewController diff --git a/RoutingExample/Routing/Routes/SignUpRoute.swift b/RoutingExample/Routing/Routes/SignUpRoute.swift index 94827b7..c7e971c 100644 --- a/RoutingExample/Routing/Routes/SignUpRoute.swift +++ b/RoutingExample/Routing/Routes/SignUpRoute.swift @@ -17,13 +17,18 @@ extension SignUpRoute where Self: Router { // internally by instances that conform to Router, like DefaultRouter, // DeeplinkRouter and others. func openSignUp(with transition: Transition) { - // If the `Router` makes use of a DI container it can resolve - // the dependencies in a clean and testable way by doing something like: - // - // let viewController = container.resolve(SignUpViewController.self, argument: router) + // Passing the current Router's container into the newly created Router below. + let router = DefaultRouter(rootTransition: transition, container: container) - let router = DefaultRouter(rootTransition: transition) - let viewModel = SignUpViewModel(router: router) + // The SignUpViewModelInterface was registered expecting an argument of the + // type SignUpViewModel.Routes. Even though DefaultRouter conforms to it, + // ultimately it is of a different type, so we need to erase its type. + let routes = router as SignUpViewModel.Routes + + // Resolve the dependency by returning an instance that conforms to + // SignUpViewModelInterface. That may be a real or mock instance. + // This is registered in the DependencyGraph.swift. + let viewModel = container.resolve(SignUpViewModelInterface.self, argument: routes) let viewController = SignUpViewController(viewModel: viewModel) router.root = viewController diff --git a/RoutingExample/Routing/Routes/Tabs/ShopTabRoute.swift b/RoutingExample/Routing/Routes/Tabs/ShopTabRoute.swift index 5972c86..8dcac8a 100644 --- a/RoutingExample/Routing/Routes/Tabs/ShopTabRoute.swift +++ b/RoutingExample/Routing/Routes/Tabs/ShopTabRoute.swift @@ -7,6 +7,7 @@ // import UIKit +import DependencyContainer protocol ShopTabRoute { func makeShopTab() -> UIViewController @@ -15,8 +16,18 @@ protocol ShopTabRoute { extension ShopTabRoute where Self: Router { func makeShopTab() -> UIViewController { // No transitions since these are managed by the TabBarController - let router = DefaultRouter(rootTransition: EmptyTransition()) - let viewModel = ShopViewModel(router: router) + // Passing the current Router's container into the newly created Router below. + let router = DefaultRouter(rootTransition: EmptyTransition(), container: container) + + // The ShopViewModelInterface was registered expecting an argument of the + // type ShopViewModel.Routes. Even though DefaultRouter conforms to it, + // ultimately it is of a different type, so we need to erase its type. + let routes = router as ShopViewModel.Routes + + // Resolve the dependency by returning an instance that conforms to + // ShopViewModelInterface. That may be a real or mock instance. + // This is registered in the DependencyGraph.swift. + let viewModel = container.resolve(ShopViewModelInterface.self, argument: routes) let viewControlle = ShopViewController(viewModel: viewModel) router.root = viewControlle diff --git a/RoutingExample/Routing/Routes/Tabs/WishlistTabRoute.swift b/RoutingExample/Routing/Routes/Tabs/WishlistTabRoute.swift index 54003b3..d0910f6 100644 --- a/RoutingExample/Routing/Routes/Tabs/WishlistTabRoute.swift +++ b/RoutingExample/Routing/Routes/Tabs/WishlistTabRoute.swift @@ -7,6 +7,7 @@ // import UIKit +import DependencyContainer protocol WishlistTabRoute { func makeWishlistTab() -> UIViewController @@ -15,8 +16,18 @@ protocol WishlistTabRoute { extension WishlistTabRoute where Self: Router { func makeWishlistTab() -> UIViewController { // No transitions since these are managed by the TabBarController - let router = DefaultRouter(rootTransition: EmptyTransition()) - let viewModel = WishlistViewModel(router: router) + // Passing the current Router's container into the newly created Router below. + let router = DefaultRouter(rootTransition: EmptyTransition(), container: container) + + // The WishlistViewModelInterface was registered expecting an argument of the + // type WishlistViewModel.Routes. Even though DefaultRouter conforms to it, + // ultimately it is of a different type, so we need to erase its type. + let routes = router as WishlistViewModel.Routes + + // Resolve the dependency by returning an instance that conforms to + // WishlistViewModelInterface. That may be a real or mock instance. + // This is registered in the DependencyGraph.swift. + let viewModel = container.resolve(WishlistViewModelInterface.self, argument: routes) let viewController = WishlistViewController(viewModel: viewModel) router.root = viewController diff --git a/RoutingExample/SceneDelegate.swift b/RoutingExample/SceneDelegate.swift index 224222d..17fd24e 100644 --- a/RoutingExample/SceneDelegate.swift +++ b/RoutingExample/SceneDelegate.swift @@ -7,15 +7,29 @@ // import UIKit +import DependencyContainer class SceneDelegate: UIResponder, UIWindowSceneDelegate { + // The Graph's container should register and resolve all dependencies of the app. + // In order to do it the container must be passed down to every Router. + // No other classes besides Routers should have access to it. Routers are the only + // responsible for instantiating new screens and they are in charge of injecting + // all necessary dependencies into them. + private let dependencyGraph = DependencyGraph() private var deeplinkRouter: Router? var window: UIWindow? + override init() { + super.init() + // Registers all dependencies of the app in the app's initialisation + dependencyGraph.registerDependencies() + } + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } - let mainRouter = DefaultRouter(rootTransition: EmptyTransition()) + // Passing the container into the mainRouter, which will inject it into the new routers. + let mainRouter = DefaultRouter(rootTransition: EmptyTransition(), container: dependencyGraph.container) let tabs = [mainRouter.makeShopTab(), mainRouter.makeWishlistTab()] window = UIWindow(frame: windowScene.coordinateSpace.bounds) @@ -37,7 +51,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { // Define all deeplinks to be opened with a Modal Transition let transition = ModalTransition() - let router = DeeplinkRouter(rootTransition: transition) + let router = DeeplinkRouter(rootTransition: transition, container: self.dependencyGraph.container) router.root = self.window?.topMostViewController() router.route(to: url, as: transition) self.deeplinkRouter = router diff --git a/RoutingExample/Tabs/ShopViewController.swift b/RoutingExample/Tabs/ShopViewController.swift index f8b262e..61f331e 100644 --- a/RoutingExample/Tabs/ShopViewController.swift +++ b/RoutingExample/Tabs/ShopViewController.swift @@ -9,9 +9,9 @@ import UIKit final class ShopViewController: UIViewController { - private let viewModel: ShopViewModel + private let viewModel: ShopViewModelInterface - init(viewModel: ShopViewModel) { + init(viewModel: ShopViewModelInterface) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } diff --git a/RoutingExample/Tabs/ShopViewModel.swift b/RoutingExample/Tabs/ShopViewModel.swift index 9d68a30..b07093f 100644 --- a/RoutingExample/Tabs/ShopViewModel.swift +++ b/RoutingExample/Tabs/ShopViewModel.swift @@ -8,7 +8,11 @@ import Foundation -final class ShopViewModel { +protocol ShopViewModelInterface { + func productButtonTouchUpInside() +} + +final class ShopViewModel: ShopViewModelInterface { typealias Routes = ProductRoute private let router: Routes diff --git a/RoutingExample/Tabs/WishlistViewController.swift b/RoutingExample/Tabs/WishlistViewController.swift index 06111b9..034d161 100644 --- a/RoutingExample/Tabs/WishlistViewController.swift +++ b/RoutingExample/Tabs/WishlistViewController.swift @@ -9,9 +9,9 @@ import UIKit final class WishlistViewController: UIViewController { - private let viewModel: WishlistViewModel + private let viewModel: WishlistViewModelInterface - init(viewModel: WishlistViewModel) { + init(viewModel: WishlistViewModelInterface) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } diff --git a/RoutingExample/Tabs/WishlistViewModel.swift b/RoutingExample/Tabs/WishlistViewModel.swift index edd68de..519bd74 100644 --- a/RoutingExample/Tabs/WishlistViewModel.swift +++ b/RoutingExample/Tabs/WishlistViewModel.swift @@ -8,7 +8,12 @@ import Foundation -final class WishlistViewModel { +protocol WishlistViewModelInterface { + func productButtonTouchUpInside() + func loginButtonTouchUpInside() +} + +final class WishlistViewModel: WishlistViewModelInterface { typealias Routes = LoginRoute & ProductRoute private let router: Routes