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