diff --git a/.github/workflows/pr_status.yml b/.github/workflows/pr_status.yml index 15bf55b..20bcb29 100644 --- a/.github/workflows/pr_status.yml +++ b/.github/workflows/pr_status.yml @@ -24,6 +24,8 @@ jobs: - 'Projects/DependencyInjection/**' design_system: - 'Projects/DesignSystem/**' + home: + - 'Projects/Home/**' - name: Generate matrix id: matrix run: | @@ -39,6 +41,10 @@ jobs: if [ "${{ steps.filter.outputs.design_system }}" == "true" ]; then targets+=("DesignSystem") fi + + if [ "${{ steps.filter.outputs.home }}" == "true" ]; then + targets+=("Home") + fi if [ ${#targets[@]} -gt 0 ]; then matrix="{\"target\": [$(printf '"%s",' "${targets[@]}" | sed 's/,$//')]}" diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 75fc026..bde3044 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -7,11 +7,25 @@ let dependecies: [TargetDependency] = [ .project(target: "DependencyInjection", path: "../DependencyInjection"), .project(target: "DependencyInjectionInterfaces", - path: "../DependencyInjection") + path: "../DependencyInjection"), + .project(target: "Home", + path: "../Home"), + .project(target: "HomeInterfaces", + path: "../Home"), + .project(target: "NavigationInterfaces", + path: "../Navigation"), + .external(name: "Networking"), + .external(name: "NetworkingInterfaces"), ] let testDependencies: [TargetDependency] = [ .target(name: moduleName), + .project(target: "DependencyInjectionTesting", + path: "../DependencyInjection"), + .project(target: "HomeTesting", + path: "../Home"), + .project(target: "NavigationTesting", + path: "../Navigation"), ] let project = Project( @@ -59,8 +73,8 @@ let project = Project( "\(moduleName)Tests", // Disabled UITests for now, as they are not yet implemented // "\(moduleName)UITests" - ] - ), + ], + arguments: .arguments(environmentVariables: ["IS_TESTING": "true"])), runAction: .runAction(configuration: .debug, executable: .project(path: ".", target: moduleName), arguments: .arguments(environmentVariables: developmentEnv))) diff --git a/Projects/App/Sources/Application/AppDelegate.swift b/Projects/App/Sources/Application/AppDelegate.swift index 2f123d2..70912bf 100644 --- a/Projects/App/Sources/Application/AppDelegate.swift +++ b/Projects/App/Sources/Application/AppDelegate.swift @@ -32,8 +32,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {} @MainActor - private func setupDependencies() { + func setupDependencies() { let injector = Injector() SharedContainer.shared.setInjector(injector) + + NetworkAssembly(injector: injector).register() + CoordinatorAssembly(injector: injector).register() } } diff --git a/Projects/App/Sources/Application/SceneDelegate.swift b/Projects/App/Sources/Application/SceneDelegate.swift index 18a4ed2..532eb1c 100644 --- a/Projects/App/Sources/Application/SceneDelegate.swift +++ b/Projects/App/Sources/Application/SceneDelegate.swift @@ -6,30 +6,38 @@ // import UIKit +import DependencyInjectionInterfaces class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? + var appCoordinator: AppCoordinating? + + #if DEBUG + var isTesting: Bool { + return ProcessInfo.processInfo.environment["IS_TESTING"] == "true" + } + #endif func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } + #if DEBUG + if isTesting { return } + #endif + let window = UIWindow(windowScene: windowScene) - let nav = UINavigationController(rootViewController: ViewController()) + let nav = UINavigationController() + let resolver = SharedContainer.shared.resolver() + + appCoordinator = resolver.resolve(AppCoordinating.self, argument: nav) + appCoordinator?.start() + window.rootViewController = nav window.makeKeyAndVisible() + self.window = window } - - func sceneDidDisconnect(_ scene: UIScene) {} - - func sceneDidBecomeActive(_ scene: UIScene) {} - - func sceneWillResignActive(_ scene: UIScene) {} - - func sceneWillEnterForeground(_ scene: UIScene) {} - - func sceneDidEnterBackground(_ scene: UIScene) {} } diff --git a/Projects/App/Sources/Assemblies/CoordinatorAssembly.swift b/Projects/App/Sources/Assemblies/CoordinatorAssembly.swift new file mode 100644 index 0000000..6294188 --- /dev/null +++ b/Projects/App/Sources/Assemblies/CoordinatorAssembly.swift @@ -0,0 +1,35 @@ +// +// +// Created by Vitor Conceicao. +// +// github.com/vitor-rc1 +// +// +import DependencyInjectionInterfaces +import HomeInterfaces +import Home +import NavigationInterfaces + +import UIKit + +public struct CoordinatorAssembly { + private let injector: DependencyInjector + + public init(injector: DependencyInjector) { + self.injector = injector + } + + @MainActor + public func register() { + injector.register(AppCoordinating.self) { (_, arg: UINavigationController) in + AppCoordinator(navigationController: arg) + } + + injector.register(HomeCoordinating.self) { (_, arg: (UINavigationController, Coordinator?)) in + let (navigationController, parentCoordinator) = arg + + return HomeCoordinator(parentCoordinator: parentCoordinator, + navigationController: navigationController) + } + } +} diff --git a/Projects/App/Sources/Assemblies/NetworkAssembly.swift.swift b/Projects/App/Sources/Assemblies/NetworkAssembly.swift.swift new file mode 100644 index 0000000..8c5ca5f --- /dev/null +++ b/Projects/App/Sources/Assemblies/NetworkAssembly.swift.swift @@ -0,0 +1,27 @@ +// +// +// Created by Vitor Conceicao. +// +// github.com/vitor-rc1 +// +// + +import DependencyInjectionInterfaces +import NetworkingInterfaces +import Networking + +import Foundation + +public struct NetworkAssembly { + private let injector: DependencyInjector + + public init(injector: DependencyInjector) { + self.injector = injector + } + + public func register() { + injector.register(NetworkServiceProtocol.self) { _ in + NetworkService() + } + } +} diff --git a/Projects/App/Sources/Coordinator/AppCoordinator.swift b/Projects/App/Sources/Coordinator/AppCoordinator.swift new file mode 100644 index 0000000..124ee30 --- /dev/null +++ b/Projects/App/Sources/Coordinator/AppCoordinator.swift @@ -0,0 +1,35 @@ +// +// +// Created by Vitor Conceicao. +// +// github.com/vitor-rc1 +// +// + +import DependencyInjectionInterfaces +import HomeInterfaces +import NavigationInterfaces +import UIKit + +protocol AppCoordinating: Coordinator {} + +@MainActor +final class AppCoordinator: AppCoordinating { + var parentCoordinator: Coordinator? + var children: [Coordinator] = [] + var navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func start() { + let resolver = SharedContainer.shared.resolver() + let arg: (UINavigationController, Coordinator?) = (navigationController, self) + + let homeCoordinator = resolver.resolve(HomeCoordinating.self, + argument: arg) + + homeCoordinator.start() + } +} diff --git a/Projects/App/Sources/ViewController.swift b/Projects/App/Sources/ViewController.swift deleted file mode 100644 index 8ee1793..0000000 --- a/Projects/App/Sources/ViewController.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ViewController.swift -// ExchangesApp -// -// Created by Vitor Conceicao on 06/02/26. -// - -import UIKit - -class ViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - view.backgroundColor = .blue - } -} diff --git a/Projects/App/Tests/Application/AppDelegateTests.swift b/Projects/App/Tests/Application/AppDelegateTests.swift index bb6a59e..9f7cd17 100644 --- a/Projects/App/Tests/Application/AppDelegateTests.swift +++ b/Projects/App/Tests/Application/AppDelegateTests.swift @@ -8,15 +8,69 @@ import Testing import DependencyInjectionInterfaces +import NetworkingInterfaces +import HomeInterfaces +import NavigationInterfaces +import UIKit + @testable import App @Suite @MainActor struct AppDelegateTests { + init() { + _ = AppDelegate() + } + @Test("GIVEN AppDelegate initialize THEN it should set injector") - func example() async throws { + func setInjectot() async throws { #expect(throws: Never.self) { _ = SharedContainer.shared.resolver() } } + + @Test("GIVEN AppDelegate initialize THEN it SHOULD register DI Network") + func networkRegister() async throws { + #expect(throws: Never.self) { + let _: NetworkServiceProtocol = SharedContainer.shared.resolver().resolve() + } + } + + @Test("GIVEN AppDelegate initialize THEN it SHOULD register DI Network") + func appCoordinatorRegister() async throws { + #expect(throws: Never.self) { + let navigation = UINavigationController() + _ = SharedContainer + .shared + .resolver() + .resolve(AppCoordinating.self, argument: navigation) + } + } + + @Test("GIVEN AppDelegate initialize THEN it SHOULD register DI AppCoordinating") + func appCoordinatingRegister() async throws { + #expect(throws: Never.self) { + let navigation = UINavigationController() + _ = SharedContainer + .shared + .resolver() + .resolve(AppCoordinating.self, argument: navigation) + } + } + + @Test("GIVEN AppDelegate initialize THEN it SHOULD register DI HomeCoordinating") + func homeCoordinatorRegister() async throws { + #expect(throws: Never.self) { + let navigation = UINavigationController() + let parentCoordinator: Coordinator? = SharedContainer + .shared + .resolver() + .resolve(AppCoordinating.self, argument: navigation) + + _ = SharedContainer + .shared + .resolver() + .resolve(HomeCoordinating.self, argument: (navigation, parentCoordinator)) + } + } } diff --git a/Projects/App/Tests/Coordinator/AppCoordinatorTests.swift b/Projects/App/Tests/Coordinator/AppCoordinatorTests.swift new file mode 100644 index 0000000..eaa720a --- /dev/null +++ b/Projects/App/Tests/Coordinator/AppCoordinatorTests.swift @@ -0,0 +1,50 @@ +// +// +// Created by Vitor Conceicao. +// +// github.com/vitor-rc1 +// +// +// +import DependencyInjectionInterfaces +import DependencyInjectionTesting +import HomeInterfaces +import HomeTesting +import NavigationInterfaces +import NavigationTesting +import Testing +import UIKit + +@testable import App + +@MainActor +@Suite("AppCoordinator Tests") +struct AppCoordinatorTests { + + // MARK: - Start Tests + + @Test("Start resolves HomeCoordinating from container") + func startResolvesHomeCoordinator() { + // Given + let navigationController = UINavigationController() + let sut = AppCoordinator(navigationController: navigationController) + + let injectorSpy = DependencyInjectorSpy() + let homeCoordinatorSpy = HomeCoordinatingSpy( + children: [], + navigationController: navigationController + ) + + injectorSpy + .register(HomeCoordinating.self) { (_, arg: (UINavigationController, Coordinator?)) in + homeCoordinatorSpy + } + + SharedContainer.shared.setInjector(injectorSpy) + + sut.start() + + #expect(injectorSpy.calledMethods.contains(.resolveWithArgument)) + #expect(homeCoordinatorSpy.calledMethods.contains(.start)) + } +} diff --git a/Projects/DependencyInjection/Project.swift b/Projects/DependencyInjection/Project.swift index 134e69f..e4eb0aa 100644 --- a/Projects/DependencyInjection/Project.swift +++ b/Projects/DependencyInjection/Project.swift @@ -8,5 +8,5 @@ let dependecies: [TargetDependency] = [ ] let project = Project.templateModule(named: moduleName, - targets: [.source,. interfaces, .test], + targets: [.source,. interfaces, .test, .testing], dependencies: dependecies) diff --git a/Projects/DependencyInjection/Testing/DependencyInjectorSpy.swift b/Projects/DependencyInjection/Testing/DependencyInjectorSpy.swift new file mode 100644 index 0000000..fb1510c --- /dev/null +++ b/Projects/DependencyInjection/Testing/DependencyInjectorSpy.swift @@ -0,0 +1,76 @@ +// +// Created by Vitor Conceicao. +// +// github.com/vitor-rc1 +// +// + +import DependencyInjectionInterfaces +import Foundation + +public final class DependencyInjectorSpy: DependencyInjector { + public init() {} + + public enum Method { + case register + case registerWithArgument + case resolve + case resolveWithArgument + } + + // Container to hold registered services and their factories + private var registeredServices: [String: Any] = [:] + private var registeredServicesWithArgument: [String: Any] = [:] + + public var calledMethods: [Method] = [] + + private func typeIdentifier(_ type: T.Type) -> String { + return String(reflecting: type) + } + + public func register(_ serviceType: Service.Type, + factory: @escaping (DependencyResolver) -> Service) { + calledMethods.append(.register) + let identifier = typeIdentifier(serviceType) + registeredServices[identifier] = factory + } + + public func register(_ serviceType: Service.Type, + factory: @escaping (DependencyResolver, Argument) -> Service) { + calledMethods.append(.registerWithArgument) + let identifier = typeIdentifier(serviceType) + registeredServicesWithArgument[identifier] = factory + } + + public func resolve() -> Service { + calledMethods.append(.resolve) + let identifier = typeIdentifier(Service.self) + + if let factory = registeredServices[identifier] as? (DependencyResolver) -> Service { + return factory(self) + } + + fatalError("No mock or registration found for \(identifier). Use stub(for:mock:) to configure a mock.") + } + + public func resolve(_ serviceType: Service.Type, + argument: Argument) -> Service { + calledMethods.append(.resolveWithArgument) + let identifier = typeIdentifier(serviceType) + + if let factory = registeredServicesWithArgument[identifier] as? (DependencyResolver, Argument) -> Service { + return factory(self, argument) + } + + fatalError("No mock or registration found for \(identifier). Use stub(for:mock:) to configure a mock.") + } + + // MARK: - Test Helpers + + /// Clear all registrations and mocks + public func reset() { + registeredServices.removeAll() + registeredServicesWithArgument.removeAll() + calledMethods.removeAll() + } +} diff --git a/Projects/Home/Interfaces/HomeCoordinating.swift b/Projects/Home/Interfaces/HomeCoordinating.swift new file mode 100644 index 0000000..651ad83 --- /dev/null +++ b/Projects/Home/Interfaces/HomeCoordinating.swift @@ -0,0 +1,12 @@ +// +// +// Created by Vitor Conceicao. +// +// github.com/vitor-rc1 +// +// + +import NavigationInterfaces +import UIKit + +public protocol HomeCoordinating: Coordinator {} diff --git a/Projects/Home/Project.swift b/Projects/Home/Project.swift new file mode 100644 index 0000000..b1e2751 --- /dev/null +++ b/Projects/Home/Project.swift @@ -0,0 +1,22 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let moduleName = "Home" + +let dependecies: [TargetDependency] = [ + .project(target: "DependencyInjectionInterfaces", + path: "../DependencyInjection"), + .project(target: "DesignSystem", + path: "../DesignSystem"), + .external(name: "NetworkingInterfaces"), +] + +let interfaceDependecies: [TargetDependency] = [ + .project(target: "NavigationInterfaces", + path: "../Navigation"), +] + +let project = Project.templateModule(named: moduleName, + targets: [.source,. interfaces, .test, .testing], + dependencies: dependecies, + interfaceDependecies: interfaceDependecies) diff --git a/Projects/Home/Sources/Coordinator/HomeCoordinator.swift b/Projects/Home/Sources/Coordinator/HomeCoordinator.swift new file mode 100644 index 0000000..718b789 --- /dev/null +++ b/Projects/Home/Sources/Coordinator/HomeCoordinator.swift @@ -0,0 +1,27 @@ +// +// +// Created by Vitor Conceicao. +// +// github.com/vitor-rc1 +// +// + +import HomeInterfaces +import NavigationInterfaces +import UIKit + +public final class HomeCoordinator: HomeCoordinating { + public var parentCoordinator: (Coordinator)? + public var children: [Coordinator] = [] + public var navigationController: UINavigationController + + public init(parentCoordinator: Coordinator? = nil, + navigationController: UINavigationController) { + self.parentCoordinator = parentCoordinator + self.navigationController = navigationController + } + + public func start() { + + } +} diff --git a/Projects/Home/Testing/Coordinator/HomeCoordinatingSpy.swift b/Projects/Home/Testing/Coordinator/HomeCoordinatingSpy.swift new file mode 100644 index 0000000..218d4cf --- /dev/null +++ b/Projects/Home/Testing/Coordinator/HomeCoordinatingSpy.swift @@ -0,0 +1,34 @@ +// +// +// Created by Vitor Conceicao. +// +// github.com/vitor-rc1 +// +// + +import HomeInterfaces +import NavigationInterfaces +import UIKit + +public final class HomeCoordinatingSpy: HomeCoordinating { + public enum Method { + case start + } + + public var calledMethods: [Method] = [] + public var parentCoordinator: Coordinator? + public var children: [Coordinator] = [] + public var navigationController: UINavigationController + + public init(parentCoordinator: Coordinator? = nil, + children: [Coordinator], + navigationController: UINavigationController) { + self.parentCoordinator = parentCoordinator + self.children = children + self.navigationController = navigationController + } + + public func start() { + calledMethods.append(.start) + } +} diff --git a/Projects/Home/Tests/TestPlaceHolder.swift b/Projects/Home/Tests/TestPlaceHolder.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Navigation/Interfaces/Coordinator.swift b/Projects/Navigation/Interfaces/Coordinator.swift new file mode 100644 index 0000000..27cfa90 --- /dev/null +++ b/Projects/Navigation/Interfaces/Coordinator.swift @@ -0,0 +1,18 @@ +// +// +// Created by Vitor Conceicao. +// +// github.com/vitor-rc1 +// +// + +import UIKit + +@MainActor +public protocol Coordinator: AnyObject { + var parentCoordinator: Coordinator? { get set } + var children: [Coordinator] { get set } + var navigationController: UINavigationController { get set } + + func start() +} diff --git a/Projects/Navigation/Project.swift b/Projects/Navigation/Project.swift new file mode 100644 index 0000000..e594ae8 --- /dev/null +++ b/Projects/Navigation/Project.swift @@ -0,0 +1,10 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let moduleName = "Navigation" + +let dependecies: [TargetDependency] = [] + +let project = Project.templateModule(named: moduleName, + targets: [.interfaces, .testing], + dependencies: dependecies) diff --git a/Projects/Navigation/Testing/CoordinatorSpy.swift b/Projects/Navigation/Testing/CoordinatorSpy.swift new file mode 100644 index 0000000..2c0ee6f --- /dev/null +++ b/Projects/Navigation/Testing/CoordinatorSpy.swift @@ -0,0 +1,33 @@ +// +// +// Created by Vitor Conceicao. +// +// github.com/vitor-rc1 +// +// + +import NavigationInterfaces +import UIKit + +public final class CoordinatorSpy: Coordinator { + public enum Method { + case start + } + + public var calledMethods: [Method] = [] + public var parentCoordinator: Coordinator? + public var children: [Coordinator] = [] + public var navigationController: UINavigationController + + public init(parentCoordinator: Coordinator? = nil, + children: [Coordinator], + navigationController: UINavigationController) { + self.parentCoordinator = parentCoordinator + self.children = children + self.navigationController = navigationController + } + + public func start() { + calledMethods.append(.start) + } +} diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index f640ad5..3cbbe40 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "networking-package", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vitor-rc1/networking-package.git", + "state" : { + "revision" : "b5498ee2ab408592d4db9a9c95faec3a08de9ea2", + "version" : "1.1.0" + } + }, { "identity" : "swift-custom-dump", "kind" : "remoteSourceControl", diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 9ca806e..f545677 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -13,6 +13,7 @@ let package = Package( name: "ExchangesApp", dependencies: [ .package(url: "https://github.com/Swinject/Swinject.git", from: "2.9.1"), - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.18.9") + .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.18.9"), + .package(url: "https://github.com/vitor-rc1/networking-package.git", from: "1.1.0") ] ) diff --git a/Tuist/ProjectDescriptionHelpers/Project+Template.swift b/Tuist/ProjectDescriptionHelpers/Project+Template.swift index 241da1b..996e655 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Template.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Template.swift @@ -10,7 +10,7 @@ public let developmentEnv: [String: EnvironmentVariable] = [ "CM_API_BASE_URL": cmApiBaseURL, ] -public let iOSDeploymentTarget: DeploymentTargets = .iOS("14.0") +public let iOSDeploymentTarget: DeploymentTargets = .iOS("15.0") public let commonSettings: SettingsDictionary = [ "SWIFT_VERSION": "6.0" ] @@ -24,6 +24,7 @@ public extension Project { targets: [ModuleTargets] = [.source, .interfaces, .test], dependencies: [TargetDependency] = [], testDependencies: [TargetDependency] = [], + interfaceDependecies: [TargetDependency] = [], shouldSetEnvVars: Bool = false) -> Project { var selectedTargets: [Target] = [] @@ -38,7 +39,8 @@ public extension Project { buildableFolders: ["Interfaces"], scripts: [ swiftLintScript - ]) + ], + dependencies: interfaceDependecies) ) } diff --git a/Workspace.swift b/Workspace.swift index 24febbc..de4324c 100644 --- a/Workspace.swift +++ b/Workspace.swift @@ -6,7 +6,8 @@ let workspace = Workspace( projects: [ "./Projects/App", "./Projects/DependencyInjection", - "./Projects/DesignSystem" + "./Projects/DesignSystem", + "./Projects/Home" ], schemes: [], generationOptions: .options(autogeneratedWorkspaceSchemes: .disabled)