From 43c156a2c3523cbd92a3566a839a0622a4b9fffb Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 11 Apr 2023 22:14:40 +0000 Subject: [PATCH] [Setup] App architecture (#9) This PR contains the work done to put in the app an MVVM-C architecture plus other small fixes in the `Library` libraries. To give further details about the work done: - [x] remove the `SceneDelegate` delegate; - [x] implemented the `WindowRouter`, `PushNavigationRouter`, and `ModalNavigationRouter` routers in the `Core` library; - [x] defined the `LocationsAddCoordination` and `LocationsListCoordination` protocols; - [x] defined the `LocationsAddViewModeling` and `LocationsListViewModeling` protocols; - [x] implemented the `LocationsListCoordinator` and `LocationsAddCoordinator` coordinators; - [x] implemented the `LocationsAddViewController` view controller and `LocationsAddViewModel` view model; - [x] implemented the `LocationsListViewController` view controller and `LocationsListViewModel` view model; - [x] implemented the `BaseViewController` base view controller; - [x] implemented the `persistence` and `remote` properties in the `DependencyService+Keys` extension. Co-authored-by: Javier Cicchelli Reviewed-on: https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/pulls/9 --- .../Sources/Core/Protocols/View.swift | 17 --- .../Sources/Core/Protocols/ViewModel.swift | 17 --- .../Core/Routers/BaseNavigationRouter.swift | 68 ++++++++++ .../Core/Routers/ModalNavigationRouter.swift | 85 ++++++++++++ .../Core/Routers/NavigationRouter.swift | 105 -------------- .../Core/Routers/PushNavigationRouter.swift | 64 +++++++++ .../Sources/Core/Routers/WindowRouter.swift | 43 ++++++ ...cationsClient.swift => RemoteClient.swift} | 8 +- .../Endpoints/GetLocationsEndpoint.swift | 2 +- .../Remote/Extensions/String+Constants.swift | 2 +- .../Sources/Remote/Models/Location.swift | 2 +- ...tionsService.swift => RemoteService.swift} | 8 +- Apps/Locations/Resources/Info.plist | 20 +-- Apps/Locations/Sources/AppDelegate.swift | 37 ++--- .../LocationsAddCoordinator.swift | 41 ++++++ .../LocationsListCoordinator.swift | 64 +++++++++ .../Extensions/DependencyService+Keys.swift | 35 +++++ .../LocationsAddCoordination.swift | 9 ++ .../LocationsListCoordination.swift | 15 ++ .../LocationsAddViewModeling.swift | 15 ++ .../LocationsListViewModeling.swift | 19 +++ Apps/Locations/Sources/SceneDelegate.swift | 67 --------- .../LocationsAddViewController.swift | 38 ++++++ .../LocationsAdd/LocationsAddViewModel.swift | 28 ++++ .../LocationsListViewController.swift | 56 ++++++++ .../LocationsListViewModel.swift | 36 +++++ .../View Controllers/BaseViewController.swift | 31 +++++ Apps/Locations/Sources/ViewController.swift | 19 --- DeepLinking.xcodeproj/project.pbxproj | 128 ++++++++++++++++-- 29 files changed, 793 insertions(+), 286 deletions(-) delete mode 100644 Apps/Locations/Libraries/Sources/Core/Protocols/View.swift delete mode 100644 Apps/Locations/Libraries/Sources/Core/Protocols/ViewModel.swift create mode 100644 Apps/Locations/Libraries/Sources/Core/Routers/BaseNavigationRouter.swift create mode 100644 Apps/Locations/Libraries/Sources/Core/Routers/ModalNavigationRouter.swift delete mode 100644 Apps/Locations/Libraries/Sources/Core/Routers/NavigationRouter.swift create mode 100644 Apps/Locations/Libraries/Sources/Core/Routers/PushNavigationRouter.swift create mode 100644 Apps/Locations/Libraries/Sources/Core/Routers/WindowRouter.swift rename Apps/Locations/Libraries/Sources/Remote/Clients/{LocationsClient.swift => RemoteClient.swift} (93%) rename Apps/Locations/Libraries/Sources/Remote/Services/{LocationsService.swift => RemoteService.swift} (82%) create mode 100644 Apps/Locations/Sources/Coordinators/LocationsAddCoordinator.swift create mode 100644 Apps/Locations/Sources/Coordinators/LocationsListCoordinator.swift create mode 100644 Apps/Locations/Sources/Extensions/DependencyService+Keys.swift create mode 100644 Apps/Locations/Sources/Protocols/Coordination/LocationsAddCoordination.swift create mode 100644 Apps/Locations/Sources/Protocols/Coordination/LocationsListCoordination.swift create mode 100644 Apps/Locations/Sources/Protocols/ViewModeling/LocationsAddViewModeling.swift create mode 100644 Apps/Locations/Sources/Protocols/ViewModeling/LocationsListViewModeling.swift delete mode 100644 Apps/Locations/Sources/SceneDelegate.swift create mode 100644 Apps/Locations/Sources/Screens/LocationsAdd/LocationsAddViewController.swift create mode 100644 Apps/Locations/Sources/Screens/LocationsAdd/LocationsAddViewModel.swift create mode 100644 Apps/Locations/Sources/Screens/LocationsList/LocationsListViewController.swift create mode 100644 Apps/Locations/Sources/Screens/LocationsList/LocationsListViewModel.swift create mode 100644 Apps/Locations/Sources/View Controllers/BaseViewController.swift delete mode 100644 Apps/Locations/Sources/ViewController.swift diff --git a/Apps/Locations/Libraries/Sources/Core/Protocols/View.swift b/Apps/Locations/Libraries/Sources/Core/Protocols/View.swift deleted file mode 100644 index 43c0b70..0000000 --- a/Apps/Locations/Libraries/Sources/Core/Protocols/View.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// View.swift -// Core -// -// Created by Javier Cicchelli on 11/04/2023. -// Copyright © 2023 Röck+Cöde. All rights reserved. -// - -/// This protocol defines the view of the **MVVM** architecture. -public protocol View { - - // MARK: Properties - - /// The view model related to the view. - var viewModel: ViewModel { get set } - -} diff --git a/Apps/Locations/Libraries/Sources/Core/Protocols/ViewModel.swift b/Apps/Locations/Libraries/Sources/Core/Protocols/ViewModel.swift deleted file mode 100644 index a550329..0000000 --- a/Apps/Locations/Libraries/Sources/Core/Protocols/ViewModel.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ViewModel.swift -// Core -// -// Created by Javier Cicchelli on 11/04/2023. -// Copyright © 2023 Röck+Cöde. All rights reserved. -// - -/// This protocol defines the view model of the **MVVM** architecture. -public protocol ViewModel: AnyObject { - - // MARK: Properties - - /// The reference to the coordinator that initialised the view model. - var coordinator: Coordinator { get set } - -} diff --git a/Apps/Locations/Libraries/Sources/Core/Routers/BaseNavigationRouter.swift b/Apps/Locations/Libraries/Sources/Core/Routers/BaseNavigationRouter.swift new file mode 100644 index 0000000..411b13a --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Core/Routers/BaseNavigationRouter.swift @@ -0,0 +1,68 @@ +// +// BaseNavigationRouter.swift +// Core +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import UIKit + +/// This is a base class for the `NavigationRouter` concrete router implementations. +public class BaseNavigationRouter: NSObject { + + // MARK: Properties + + /// A navigation controller to use within this concrete router. + var navigationController: UINavigationController + + /// Dictionary that persist `onDismiss` closure for its respective view controllers until one of the later is dismissed. + var onDismissForViewController: [UIViewController: Router.OnDismissedClosure] = [:] + + // MARK: Initialisers + + /// Initialise this router. + /// - Parameter navigationController: A `UINavigationController` navigation controller instance to use in this router. + init(navigationController: UINavigationController) { + self.navigationController = navigationController + + super.init() + + self.navigationController.delegate = self + } + + // MARK: Functions + + /// Executes the `onDismiss` closure for a given view controller. + /// - Parameter viewController: A `UIViewController` view controller instance for which the on dismiss closure will be executed. + func performOnDismissed(for viewController: UIViewController) { + guard let onDismiss = onDismissForViewController[viewController] else { + return + } + + onDismiss() + + onDismissForViewController[viewController] = nil + } + +} + +// MARK: - UINavigationControllerDelegate + +extension BaseNavigationRouter: UINavigationControllerDelegate { + + // MARK: Functions + + public func navigationController( + _ navigationController: UINavigationController, + didShow viewController: UIViewController, + animated: Bool + ) { + guard let dismissedViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else { + return + } + + performOnDismissed(for: dismissedViewController) + } + +} diff --git a/Apps/Locations/Libraries/Sources/Core/Routers/ModalNavigationRouter.swift b/Apps/Locations/Libraries/Sources/Core/Routers/ModalNavigationRouter.swift new file mode 100644 index 0000000..b80f2df --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Core/Routers/ModalNavigationRouter.swift @@ -0,0 +1,85 @@ +// +// ModalNavigationRouter.swift +// Core +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import UIKit + +public class ModalNavigationRouter: BaseNavigationRouter { + + // MARK: Properties + + /// The parent view controller from where this router is being called from. + public unowned let parentViewController: UIViewController + + // MARK: Initialisers + + /// Initialise this router. + /// - Parameter parentViewController: A `UIViewController` view controller instance from where this router is originated. + public init(parentViewController: UIViewController) { + self.parentViewController = parentViewController + + super.init(navigationController: .init()) + } + +} + +extension ModalNavigationRouter: Router { + public func present( + _ viewController: UIViewController, + animated: Bool, + onDismiss: OnDismissedClosure? + ) { + onDismissForViewController[viewController] = onDismiss + + if navigationController.viewControllers.isEmpty { + presentModally(viewController, animated: animated) + } else { + navigationController.pushViewController(viewController, animated: animated) + } + } + + public func dismiss(animated: Bool) { + guard let firstViewController = navigationController.viewControllers.first else { + return + } + + performOnDismissed(for: firstViewController) + + parentViewController.dismiss(animated: animated) + } + +} + +// MARK: - Helpers + +private extension ModalNavigationRouter { + + // MARK: Functions + + func presentModally(_ viewController: UIViewController, animated: Bool) { + viewController.navigationItem.leftBarButtonItem = UIBarButtonItem( + title: "Cancel", + style: .plain, + target: self, + action: #selector(onCancelPressed) + ) + + navigationController.setViewControllers([viewController], animated: false) + + parentViewController.present(navigationController, animated: animated) + } + + @objc func onCancelPressed() { + guard let firstViewController = navigationController.viewControllers.first else { + return + } + + performOnDismissed(for: firstViewController) + dismiss(animated: true) + } + +} diff --git a/Apps/Locations/Libraries/Sources/Core/Routers/NavigationRouter.swift b/Apps/Locations/Libraries/Sources/Core/Routers/NavigationRouter.swift deleted file mode 100644 index 688c8ce..0000000 --- a/Apps/Locations/Libraries/Sources/Core/Routers/NavigationRouter.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// NavigationRouter.swift -// Core -// -// Created by Javier Cicchelli on 11/04/2023. -// Copyright © 2023 Röck+Cöde. All rights reserved. -// - -import UIKit - -/// This class is responsible for presenting view controllers, as it is a concrete implementation of the `Router` protocol, but it won't know what view controller or which view controller is next. -public class NavigationRouter: NSObject { - - // MARK: Properties - - /// A navigation controller to use within this concrete router. - private let navigationController: UINavigationController - - /// A root view controller coming in from the navigation controller, if any. - private let rootViewController: UIViewController? - - /// Dictionary that persist `onDismiss` closure for its respective view controllers until one of the later is dismissed. - private var onDismissForViewController: [UIViewController: Router.OnDismissedClosure] = [:] - - // MARK: Initialisers - - /// Initialise this router. - /// - Parameter navigationController: A `UINavigationController` navigation controller instance to use in this router. - public init(navigationController: UINavigationController) { - self.navigationController = navigationController - self.rootViewController = navigationController.viewControllers.first - - super.init() - - self.navigationController.delegate = self - } - -} - -// MARK: - Router - -extension NavigationRouter: Router { - - // MARK: Functions - - public func present( - _ viewController: UIViewController, - animated: Bool, - onDismiss: OnDismissedClosure? - ) { - onDismissForViewController[viewController] = onDismiss - - navigationController.pushViewController(viewController, animated: animated) - } - - public func dismiss(animated: Bool) { - guard let rootViewController else { - navigationController.popViewController(animated: animated) - return - } - - performOnDismissed(for: rootViewController) - - navigationController.popToViewController(rootViewController, animated: animated) - } - -} - -// MARK: - UINavigationControllerDelegate - -extension NavigationRouter: UINavigationControllerDelegate { - - // MARK: Functions - - public func navigationController( - _ navigationController: UINavigationController, - didShow viewController: UIViewController, - animated: Bool - ) { - guard let dismissedViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else { - return - } - - performOnDismissed(for: dismissedViewController) - } - -} - -// MARK: - Helpers - -private extension NavigationRouter { - - // MARK: Functions - - func performOnDismissed(for viewController: UIViewController) { - guard let onDismiss = onDismissForViewController[viewController] else { - return - } - - onDismiss() - - onDismissForViewController[viewController] = nil - } - -} diff --git a/Apps/Locations/Libraries/Sources/Core/Routers/PushNavigationRouter.swift b/Apps/Locations/Libraries/Sources/Core/Routers/PushNavigationRouter.swift new file mode 100644 index 0000000..4d9b77b --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Core/Routers/PushNavigationRouter.swift @@ -0,0 +1,64 @@ +// +// PushNavigationRouter.swift +// Core +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import UIKit + +/// This class is responsible for presenting view controllers, as it is a concrete implementation of the `Router` protocol, but it won't know what view controller or which view controller is next. +public class PushNavigationRouter: BaseNavigationRouter { + + // MARK: Properties + + /// A root view controller coming in from the navigation controller, if any. + private let rootViewController: UIViewController? + + // MARK: Initialisers + + /// Initialise this router. + /// - Parameters: + /// - navigationController: A `UINavigationController` navigation controller instance to use in this router. + /// - rootViewController: A `UIViewController` view controller instance to define as a root view controller of the navigation controller. + /// - Note This initialiser added the `rootViewController` parameter although it is not really needed to differentiate itself from the `.init(navigationController:)` implemented for the `BaseNavigationRouter` base class. + public init( + navigationController: UINavigationController, + rootViewController: UIViewController? = nil + ) { + self.rootViewController = navigationController.viewControllers.first ?? rootViewController + + super.init(navigationController: navigationController) + } + +} + +// MARK: - Router + +extension PushNavigationRouter: Router { + + // MARK: Functions + + public func present( + _ viewController: UIViewController, + animated: Bool, + onDismiss: OnDismissedClosure? + ) { + onDismissForViewController[viewController] = onDismiss + + navigationController.pushViewController(viewController, animated: animated) + } + + public func dismiss(animated: Bool) { + guard let rootViewController else { + navigationController.popViewController(animated: animated) + return + } + + performOnDismissed(for: rootViewController) + + navigationController.popToViewController(rootViewController, animated: animated) + } + +} diff --git a/Apps/Locations/Libraries/Sources/Core/Routers/WindowRouter.swift b/Apps/Locations/Libraries/Sources/Core/Routers/WindowRouter.swift new file mode 100644 index 0000000..332ffac --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Core/Routers/WindowRouter.swift @@ -0,0 +1,43 @@ +// +// WindowRouter.swift +// Core +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import UIKit + +/// This class is responsible for populating the window of an application. +public class WindowRouter: Router { + + // MARK: Properties + + /// The window to set manually with a `UIViewController` view controller instance. + private let window: UIWindow? + + // MARK: Initialisers + + /// Initialise this router. + /// - Parameter window: A `UIWindow` window instance to be set manually. + public init(window: UIWindow?) { + self.window = window + } + + // MARK: Functions + + public func present( + _ viewController: UIViewController, + animated: Bool, + onDismiss: OnDismissedClosure? + ) { + window?.rootViewController = viewController + + window?.makeKeyAndVisible() + } + + public func dismiss(animated: Bool) { + // Nothing to do here... + } + +} diff --git a/Apps/Locations/Libraries/Sources/Remote/Clients/LocationsClient.swift b/Apps/Locations/Libraries/Sources/Remote/Clients/RemoteClient.swift similarity index 93% rename from Apps/Locations/Libraries/Sources/Remote/Clients/LocationsClient.swift rename to Apps/Locations/Libraries/Sources/Remote/Clients/RemoteClient.swift index 9ed37f2..cdb996c 100644 --- a/Apps/Locations/Libraries/Sources/Remote/Clients/LocationsClient.swift +++ b/Apps/Locations/Libraries/Sources/Remote/Clients/RemoteClient.swift @@ -1,6 +1,6 @@ // -// LocationsClient.swift -// Locations +// RemoteClient.swift +// Remote // // Created by Javier Cicchelli on 10/04/2023. // Copyright © 2023 Röck+Cöde. All rights reserved. @@ -9,7 +9,7 @@ import APICore import Foundation -struct LocationsClient { +struct RemoteClient { // MARK: Properties @@ -27,7 +27,7 @@ struct LocationsClient { // MARK: - Client -extension LocationsClient: Client { +extension RemoteClient: Client { // MARK: Functions diff --git a/Apps/Locations/Libraries/Sources/Remote/Endpoints/GetLocationsEndpoint.swift b/Apps/Locations/Libraries/Sources/Remote/Endpoints/GetLocationsEndpoint.swift index 1755734..3d58fa8 100644 --- a/Apps/Locations/Libraries/Sources/Remote/Endpoints/GetLocationsEndpoint.swift +++ b/Apps/Locations/Libraries/Sources/Remote/Endpoints/GetLocationsEndpoint.swift @@ -1,6 +1,6 @@ // // GetLocationsEndpoint.swift -// Locations +// Remote // // Created by Javier Cicchelli on 10/04/2023. // Copyright © 2023 Röck+Cöde. All rights reserved. diff --git a/Apps/Locations/Libraries/Sources/Remote/Extensions/String+Constants.swift b/Apps/Locations/Libraries/Sources/Remote/Extensions/String+Constants.swift index 3eff3f9..a839fa5 100644 --- a/Apps/Locations/Libraries/Sources/Remote/Extensions/String+Constants.swift +++ b/Apps/Locations/Libraries/Sources/Remote/Extensions/String+Constants.swift @@ -1,6 +1,6 @@ // // String+Constants.swift -// Locations +// Remote // // Created by Javier Cicchelli on 10/04/2023. // Copyright © 2023 Röck+Cöde. All rights reserved. diff --git a/Apps/Locations/Libraries/Sources/Remote/Models/Location.swift b/Apps/Locations/Libraries/Sources/Remote/Models/Location.swift index 52567a0..0342f96 100644 --- a/Apps/Locations/Libraries/Sources/Remote/Models/Location.swift +++ b/Apps/Locations/Libraries/Sources/Remote/Models/Location.swift @@ -1,6 +1,6 @@ // // Location.swift -// Locations (Library) +// Remote // // Created by Javier Cicchelli on 10/04/2023. // Copyright © 2023 Röck+Cöde. All rights reserved. diff --git a/Apps/Locations/Libraries/Sources/Remote/Services/LocationsService.swift b/Apps/Locations/Libraries/Sources/Remote/Services/RemoteService.swift similarity index 82% rename from Apps/Locations/Libraries/Sources/Remote/Services/LocationsService.swift rename to Apps/Locations/Libraries/Sources/Remote/Services/RemoteService.swift index 2e102ab..b9bca95 100644 --- a/Apps/Locations/Libraries/Sources/Remote/Services/LocationsService.swift +++ b/Apps/Locations/Libraries/Sources/Remote/Services/RemoteService.swift @@ -1,6 +1,6 @@ // -// LocationsService.swift -// Locations +// RemoteService.swift +// Remote // // Created by Javier Cicchelli on 10/04/2023. // Copyright © 2023 Röck+Cöde. All rights reserved. @@ -9,7 +9,7 @@ import APICore import Foundation -public struct LocationsService { +public struct RemoteService { // MARK: Properties @@ -18,7 +18,7 @@ public struct LocationsService { // MARK: Initialisers public init(configuration: URLSessionConfiguration = .default) { - self.client = LocationsClient(configuration: configuration) + self.client = RemoteClient(configuration: configuration) } // MARK: Functions diff --git a/Apps/Locations/Resources/Info.plist b/Apps/Locations/Resources/Info.plist index 0eb786d..0c67376 100644 --- a/Apps/Locations/Resources/Info.plist +++ b/Apps/Locations/Resources/Info.plist @@ -1,23 +1,5 @@ - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - - + diff --git a/Apps/Locations/Sources/AppDelegate.swift b/Apps/Locations/Sources/AppDelegate.swift index b40c8ba..55a61df 100644 --- a/Apps/Locations/Sources/AppDelegate.swift +++ b/Apps/Locations/Sources/AppDelegate.swift @@ -6,40 +6,31 @@ // Copyright © 2023 Röck+Cöde. All rights reserved. // +import Core import UIKit -import CoreData @main class AppDelegate: UIResponder, UIApplicationDelegate { + + // MARK: Properties + + lazy var coordinator: LocationsListCoordinator = .init(router: router) + lazy var router: WindowRouter = .init(window: window) + lazy var window: UIWindow? = .init(frame: UIScreen.main.bounds) + + // MARK: UIApplicationDelegate func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - // Override point for customization after application launch. + coordinator.present(animated: false, onDismiss: nil) + return true } - - // MARK: UISceneSession Lifecycle - - func application( - _ application: UIApplication, - configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions - ) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application( - _ application: UIApplication, - didDiscardSceneSessions sceneSessions: Set - ) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + + func applicationDidEnterBackground(_ application: UIApplication) { + // Save changes in the application's managed object context when the application transitions to the background. } } - diff --git a/Apps/Locations/Sources/Coordinators/LocationsAddCoordinator.swift b/Apps/Locations/Sources/Coordinators/LocationsAddCoordinator.swift new file mode 100644 index 0000000..61dd9c0 --- /dev/null +++ b/Apps/Locations/Sources/Coordinators/LocationsAddCoordinator.swift @@ -0,0 +1,41 @@ +// +// LocationsAddCoordinator.swift +// Locations +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Core +import UIKit + +class LocationsAddCoordinator: Coordinator { + + // MARK: Properties + + var children: [Coordinator] = [] + var router: Router + + // MARK: Initialisers + + init(router: Router) { + self.router = router + } + + // MARK: Coordinator + + func present(animated: Bool, onDismiss: (() -> Void)?) { + router.present( + LocationsAddViewController( + viewModel: LocationsAddViewModel(coordinator: self) + ), + animated: animated, + onDismiss: onDismiss + ) + } + +} + +// MARK: - LocationsAddCoordination + +extension LocationsAddCoordinator: LocationsAddCoordination {} diff --git a/Apps/Locations/Sources/Coordinators/LocationsListCoordinator.swift b/Apps/Locations/Sources/Coordinators/LocationsListCoordinator.swift new file mode 100644 index 0000000..a7d2592 --- /dev/null +++ b/Apps/Locations/Sources/Coordinators/LocationsListCoordinator.swift @@ -0,0 +1,64 @@ +// +// LocationsListCoordinator.swift +// Locations +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Core +import UIKit + +class LocationsListCoordinator: Coordinator { + + // MARK: Properties + + var children: [Coordinator] = [] + var router: Router + + private var viewController: UIViewController? + + // MARK: Initialisers + + init(router: Router) { + self.router = router + } + + // MARK: Coordinator + + func present(animated: Bool, onDismiss: (() -> Void)?) { + let navigationController = UINavigationController(rootViewController: LocationsListViewController( + viewModel: LocationsListViewModel(coordinator: self) + )) + + viewController = navigationController + + router.present( + navigationController, + animated: animated, + onDismiss: onDismiss + ) + } + +} + +// MARK: - LocationsListCoordination + +extension LocationsListCoordinator: LocationsListCoordination { + + // MARK: Functions + + func openAddLocation() { + guard let viewController else { + return + } + + present( + child: LocationsAddCoordinator( + router: ModalNavigationRouter(parentViewController: viewController) + ), + animated: true + ) + } + +} diff --git a/Apps/Locations/Sources/Extensions/DependencyService+Keys.swift b/Apps/Locations/Sources/Extensions/DependencyService+Keys.swift new file mode 100644 index 0000000..b1286d0 --- /dev/null +++ b/Apps/Locations/Sources/Extensions/DependencyService+Keys.swift @@ -0,0 +1,35 @@ +// +// DependencyService+Keys.swift +// Locations +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Dependency +import Persistence +import Remote + +// MARK: - DependencyService+Keys + +extension DependencyService { + var persistence: PersistenceService { + get { Self[PersistenceKey.self] } + set { Self[PersistenceKey.self] = newValue } + } + + var remote: RemoteService { + get { Self[RemoteKey.self] } + set { Self[RemoteKey.self] = newValue } + } +} + +// MARK: - Dependency keys + +struct PersistenceKey: DependencyKey { + static var currentValue: PersistenceService = .shared +} + +struct RemoteKey: DependencyKey { + static var currentValue: RemoteService = .init() +} diff --git a/Apps/Locations/Sources/Protocols/Coordination/LocationsAddCoordination.swift b/Apps/Locations/Sources/Protocols/Coordination/LocationsAddCoordination.swift new file mode 100644 index 0000000..6dd5bee --- /dev/null +++ b/Apps/Locations/Sources/Protocols/Coordination/LocationsAddCoordination.swift @@ -0,0 +1,9 @@ +// +// LocationsAddCoordination.swift +// Locations +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +protocol LocationsAddCoordination: AnyObject {} diff --git a/Apps/Locations/Sources/Protocols/Coordination/LocationsListCoordination.swift b/Apps/Locations/Sources/Protocols/Coordination/LocationsListCoordination.swift new file mode 100644 index 0000000..bafe329 --- /dev/null +++ b/Apps/Locations/Sources/Protocols/Coordination/LocationsListCoordination.swift @@ -0,0 +1,15 @@ +// +// LocationsListCoordination.swift +// Locations +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +protocol LocationsListCoordination: AnyObject { + + // MARK: Functions + + func openAddLocation() + +} diff --git a/Apps/Locations/Sources/Protocols/ViewModeling/LocationsAddViewModeling.swift b/Apps/Locations/Sources/Protocols/ViewModeling/LocationsAddViewModeling.swift new file mode 100644 index 0000000..b4e707c --- /dev/null +++ b/Apps/Locations/Sources/Protocols/ViewModeling/LocationsAddViewModeling.swift @@ -0,0 +1,15 @@ +// +// LocationsAddViewModeling.swift +// Locations +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +protocol LocationsAddViewModeling: AnyObject { + + // MARK: Properties + + var coordinator: LocationsAddCoordination? { get set } + +} diff --git a/Apps/Locations/Sources/Protocols/ViewModeling/LocationsListViewModeling.swift b/Apps/Locations/Sources/Protocols/ViewModeling/LocationsListViewModeling.swift new file mode 100644 index 0000000..90deeab --- /dev/null +++ b/Apps/Locations/Sources/Protocols/ViewModeling/LocationsListViewModeling.swift @@ -0,0 +1,19 @@ +// +// LocationsListViewModeling.swift +// Locations +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +protocol LocationsListViewModeling: AnyObject { + + // MARK: Properties + + var coordinator: LocationsListCoordination? { get set } + + // MARK: Functions + + func openAddLocation() + +} diff --git a/Apps/Locations/Sources/SceneDelegate.swift b/Apps/Locations/Sources/SceneDelegate.swift deleted file mode 100644 index c95cee8..0000000 --- a/Apps/Locations/Sources/SceneDelegate.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// SceneDelegate.swift -// Locations -// -// Created by Javier Cicchelli on 08/04/2023. -// Copyright © 2023 Röck+Cöde. All rights reserved. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - func scene( - _ scene: UIScene, - willConnectTo session: UISceneSession, - options connectionOptions: UIScene.ConnectionOptions - ) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let windowScene = scene as? UIWindowScene else { - return - } - - window = { - let window = UIWindow(windowScene: windowScene) - - window.rootViewController = ViewController() - window.makeKeyAndVisible() - - return window - }() - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - - // Save changes in the application's managed object context when the application transitions to the background. - } - -} diff --git a/Apps/Locations/Sources/Screens/LocationsAdd/LocationsAddViewController.swift b/Apps/Locations/Sources/Screens/LocationsAdd/LocationsAddViewController.swift new file mode 100644 index 0000000..86f4421 --- /dev/null +++ b/Apps/Locations/Sources/Screens/LocationsAdd/LocationsAddViewController.swift @@ -0,0 +1,38 @@ +// +// LocationsAddViewController.swift +// Locations +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Core +import UIKit + +class LocationsAddViewController: BaseViewController { + + // MARK: Properties + + var viewModel: LocationsAddViewModeling + + // MARK: Initialisers + + init(viewModel: LocationsAddViewModeling) { + self.viewModel = viewModel + + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Location Add" + } + +} diff --git a/Apps/Locations/Sources/Screens/LocationsAdd/LocationsAddViewModel.swift b/Apps/Locations/Sources/Screens/LocationsAdd/LocationsAddViewModel.swift new file mode 100644 index 0000000..84e9678 --- /dev/null +++ b/Apps/Locations/Sources/Screens/LocationsAdd/LocationsAddViewModel.swift @@ -0,0 +1,28 @@ +// +// LocationsAddViewModel.swift +// Locations +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Combine +import Core + +class LocationsAddViewModel: ObservableObject { + + // MARK: Properties + + weak var coordinator: LocationsAddCoordination? + + // MARK: Initialisers + + init(coordinator: LocationsAddCoordination) { + self.coordinator = coordinator + } + +} + +// MARK: - LocationsAddViewModeling + +extension LocationsAddViewModel: LocationsAddViewModeling {} diff --git a/Apps/Locations/Sources/Screens/LocationsList/LocationsListViewController.swift b/Apps/Locations/Sources/Screens/LocationsList/LocationsListViewController.swift new file mode 100644 index 0000000..cc35c26 --- /dev/null +++ b/Apps/Locations/Sources/Screens/LocationsList/LocationsListViewController.swift @@ -0,0 +1,56 @@ +// +// LocationsListViewController.swift +// Locations +// +// Created by Javier Cicchelli on 08/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Core +import UIKit + +class LocationsListViewController: BaseViewController { + + // MARK: Properties + + var viewModel: LocationsListViewModeling + + // MARK: Initialisers + + init(viewModel: LocationsListViewModeling) { + self.viewModel = viewModel + + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.rightBarButtonItem = UIBarButtonItem( + title: "Add", + style: .plain, + target: self, + action: #selector(addLocationPressed) + ) + title = "Locations" + } + +} + +// MARK: - Helpers + +private extension LocationsListViewController { + + // MARK: Functions + + @objc func addLocationPressed() { + viewModel.openAddLocation() + } + +} diff --git a/Apps/Locations/Sources/Screens/LocationsList/LocationsListViewModel.swift b/Apps/Locations/Sources/Screens/LocationsList/LocationsListViewModel.swift new file mode 100644 index 0000000..c3644fa --- /dev/null +++ b/Apps/Locations/Sources/Screens/LocationsList/LocationsListViewModel.swift @@ -0,0 +1,36 @@ +// +// LocationsListViewModel.swift +// Locations +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Combine +import Core + +class LocationsListViewModel: ObservableObject { + + // MARK: Properties + + weak var coordinator: LocationsListCoordination? + + // MARK: Initialisers + + init(coordinator: LocationsListCoordination) { + self.coordinator = coordinator + } + +} + +// MARK: - LocationsListViewModeling + +extension LocationsListViewModel: LocationsListViewModeling { + + // MARK: Functions + + func openAddLocation() { + coordinator?.openAddLocation() + } + +} diff --git a/Apps/Locations/Sources/View Controllers/BaseViewController.swift b/Apps/Locations/Sources/View Controllers/BaseViewController.swift new file mode 100644 index 0000000..ea3fec4 --- /dev/null +++ b/Apps/Locations/Sources/View Controllers/BaseViewController.swift @@ -0,0 +1,31 @@ +// +// BaseViewController.swift +// Locations +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import UIKit + +class BaseViewController: UIViewController { + + // MARK: Initialisers + + init() { + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + } + +} diff --git a/Apps/Locations/Sources/ViewController.swift b/Apps/Locations/Sources/ViewController.swift deleted file mode 100644 index 8c3be97..0000000 --- a/Apps/Locations/Sources/ViewController.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ViewController.swift -// Locations -// -// Created by Javier Cicchelli on 08/04/2023. -// Copyright © 2023 Röck+Cöde. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .red - } - -} diff --git a/DeepLinking.xcodeproj/project.pbxproj b/DeepLinking.xcodeproj/project.pbxproj index 6181e6f..33b73ae 100644 --- a/DeepLinking.xcodeproj/project.pbxproj +++ b/DeepLinking.xcodeproj/project.pbxproj @@ -7,9 +7,19 @@ objects = { /* Begin PBXBuildFile section */ + 02031EBF29E5F949003C108C /* LocationsAddViewModeling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EBE29E5F949003C108C /* LocationsAddViewModeling.swift */; }; + 02031EC629E5FEE4003C108C /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EC529E5FEE4003C108C /* BaseViewController.swift */; }; + 02031EC929E60B29003C108C /* DependencyService+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EC829E60B29003C108C /* DependencyService+Keys.swift */; }; + 46C3B7C629E5BF1500F8F57C /* LocationsListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7C529E5BF1500F8F57C /* LocationsListCoordinator.swift */; }; + 46C3B7CB29E5CD3200F8F57C /* LocationsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7CA29E5CD3200F8F57C /* LocationsListViewModel.swift */; }; + 46C3B7CF29E5D00E00F8F57C /* LocationsAddViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7CE29E5D00E00F8F57C /* LocationsAddViewModel.swift */; }; + 46C3B7D129E5D06D00F8F57C /* LocationsAddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7D029E5D06D00F8F57C /* LocationsAddViewController.swift */; }; + 46C3B7D629E5E50500F8F57C /* LocationsListViewModeling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7D529E5E50500F8F57C /* LocationsListViewModeling.swift */; }; + 46C3B7D829E5E55000F8F57C /* LocationsListCoordination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7D729E5E55000F8F57C /* LocationsListCoordination.swift */; }; + 46C3B7DC29E5ED2300F8F57C /* LocationsAddCoordination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7DB29E5ED2300F8F57C /* LocationsAddCoordination.swift */; }; + 46C3B7DE29E5ED2E00F8F57C /* LocationsAddCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7DD29E5ED2E00F8F57C /* LocationsAddCoordinator.swift */; }; 46EB331B29E1CE04001D5EAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB331A29E1CE04001D5EAF /* AppDelegate.swift */; }; - 46EB331D29E1CE04001D5EAF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB331C29E1CE04001D5EAF /* SceneDelegate.swift */; }; - 46EB331F29E1CE04001D5EAF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB331E29E1CE04001D5EAF /* ViewController.swift */; }; + 46EB331F29E1CE04001D5EAF /* LocationsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB331E29E1CE04001D5EAF /* LocationsListViewController.swift */; }; 46EB332729E1CE05001D5EAF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 46EB332629E1CE05001D5EAF /* Assets.xcassets */; }; 46EB332A29E1CE05001D5EAF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46EB332829E1CE05001D5EAF /* LaunchScreen.storyboard */; }; 46EB334429E1D1EC001D5EAF /* Libraries in Frameworks */ = {isa = PBXBuildFile; productRef = 46EB334329E1D1EC001D5EAF /* Libraries */; }; @@ -111,11 +121,21 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 02031EBE29E5F949003C108C /* LocationsAddViewModeling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsAddViewModeling.swift; sourceTree = ""; }; + 02031EC529E5FEE4003C108C /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; + 02031EC829E60B29003C108C /* DependencyService+Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DependencyService+Keys.swift"; sourceTree = ""; }; + 46C3B7C529E5BF1500F8F57C /* LocationsListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsListCoordinator.swift; sourceTree = ""; }; + 46C3B7CA29E5CD3200F8F57C /* LocationsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsListViewModel.swift; sourceTree = ""; }; + 46C3B7CE29E5D00E00F8F57C /* LocationsAddViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsAddViewModel.swift; sourceTree = ""; }; + 46C3B7D029E5D06D00F8F57C /* LocationsAddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsAddViewController.swift; sourceTree = ""; }; + 46C3B7D529E5E50500F8F57C /* LocationsListViewModeling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsListViewModeling.swift; sourceTree = ""; }; + 46C3B7D729E5E55000F8F57C /* LocationsListCoordination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsListCoordination.swift; sourceTree = ""; }; + 46C3B7DB29E5ED2300F8F57C /* LocationsAddCoordination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsAddCoordination.swift; sourceTree = ""; }; + 46C3B7DD29E5ED2E00F8F57C /* LocationsAddCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsAddCoordinator.swift; sourceTree = ""; }; 46EB325829E1BD5C001D5EAF /* Wikipedia.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Wikipedia.xcodeproj; path = Wikipedia/Wikipedia.xcodeproj; sourceTree = ""; }; 46EB331829E1CE04001D5EAF /* Locations.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Locations.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46EB331A29E1CE04001D5EAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 46EB331C29E1CE04001D5EAF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 46EB331E29E1CE04001D5EAF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 46EB331E29E1CE04001D5EAF /* LocationsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsListViewController.swift; sourceTree = ""; }; 46EB332629E1CE05001D5EAF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 46EB332929E1CE05001D5EAF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 46EB332B29E1CE05001D5EAF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -138,6 +158,85 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 02031EC429E5FEB1003C108C /* View Controllers */ = { + isa = PBXGroup; + children = ( + 02031EC529E5FEE4003C108C /* BaseViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + 02031EC729E60ADB003C108C /* Extensions */ = { + isa = PBXGroup; + children = ( + 02031EC829E60B29003C108C /* DependencyService+Keys.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 0276C96029E5F5DC000B62AF /* Protocols */ = { + isa = PBXGroup; + children = ( + 0276C96229E5F5ED000B62AF /* Coordination */, + 0276C96129E5F5E5000B62AF /* ViewModeling */, + ); + path = Protocols; + sourceTree = ""; + }; + 0276C96129E5F5E5000B62AF /* ViewModeling */ = { + isa = PBXGroup; + children = ( + 02031EBE29E5F949003C108C /* LocationsAddViewModeling.swift */, + 46C3B7D529E5E50500F8F57C /* LocationsListViewModeling.swift */, + ); + path = ViewModeling; + sourceTree = ""; + }; + 0276C96229E5F5ED000B62AF /* Coordination */ = { + isa = PBXGroup; + children = ( + 46C3B7DB29E5ED2300F8F57C /* LocationsAddCoordination.swift */, + 46C3B7D729E5E55000F8F57C /* LocationsListCoordination.swift */, + ); + path = Coordination; + sourceTree = ""; + }; + 46C3B7C429E5BEE900F8F57C /* Coordinators */ = { + isa = PBXGroup; + children = ( + 46C3B7DD29E5ED2E00F8F57C /* LocationsAddCoordinator.swift */, + 46C3B7C529E5BF1500F8F57C /* LocationsListCoordinator.swift */, + ); + path = Coordinators; + sourceTree = ""; + }; + 46C3B7C929E5CB8F00F8F57C /* Screens */ = { + isa = PBXGroup; + children = ( + 46C3B7CD29E5CFCD00F8F57C /* LocationsAdd */, + 46C3B7CC29E5CFBB00F8F57C /* LocationsList */, + ); + path = Screens; + sourceTree = ""; + }; + 46C3B7CC29E5CFBB00F8F57C /* LocationsList */ = { + isa = PBXGroup; + children = ( + 46EB331E29E1CE04001D5EAF /* LocationsListViewController.swift */, + 46C3B7CA29E5CD3200F8F57C /* LocationsListViewModel.swift */, + ); + path = LocationsList; + sourceTree = ""; + }; + 46C3B7CD29E5CFCD00F8F57C /* LocationsAdd */ = { + isa = PBXGroup; + children = ( + 46C3B7D029E5D06D00F8F57C /* LocationsAddViewController.swift */, + 46C3B7CE29E5D00E00F8F57C /* LocationsAddViewModel.swift */, + ); + path = LocationsAdd; + sourceTree = ""; + }; 46EB325029E1BBD1001D5EAF = { isa = PBXGroup; children = ( @@ -200,8 +299,11 @@ isa = PBXGroup; children = ( 46EB331A29E1CE04001D5EAF /* AppDelegate.swift */, - 46EB331C29E1CE04001D5EAF /* SceneDelegate.swift */, - 46EB331E29E1CE04001D5EAF /* ViewController.swift */, + 0276C96029E5F5DC000B62AF /* Protocols */, + 02031EC729E60ADB003C108C /* Extensions */, + 46C3B7C429E5BEE900F8F57C /* Coordinators */, + 46C3B7C929E5CB8F00F8F57C /* Screens */, + 02031EC429E5FEB1003C108C /* View Controllers */, ); path = Sources; sourceTree = ""; @@ -407,9 +509,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 46EB331F29E1CE04001D5EAF /* ViewController.swift in Sources */, + 46C3B7C629E5BF1500F8F57C /* LocationsListCoordinator.swift in Sources */, + 46EB331F29E1CE04001D5EAF /* LocationsListViewController.swift in Sources */, + 02031EC629E5FEE4003C108C /* BaseViewController.swift in Sources */, 46EB331B29E1CE04001D5EAF /* AppDelegate.swift in Sources */, - 46EB331D29E1CE04001D5EAF /* SceneDelegate.swift in Sources */, + 02031EBF29E5F949003C108C /* LocationsAddViewModeling.swift in Sources */, + 46C3B7DE29E5ED2E00F8F57C /* LocationsAddCoordinator.swift in Sources */, + 46C3B7DC29E5ED2300F8F57C /* LocationsAddCoordination.swift in Sources */, + 46C3B7D829E5E55000F8F57C /* LocationsListCoordination.swift in Sources */, + 46C3B7D629E5E50500F8F57C /* LocationsListViewModeling.swift in Sources */, + 46C3B7CF29E5D00E00F8F57C /* LocationsAddViewModel.swift in Sources */, + 02031EC929E60B29003C108C /* DependencyService+Keys.swift in Sources */, + 46C3B7D129E5D06D00F8F57C /* LocationsAddViewController.swift in Sources */, + 46C3B7CB29E5CD3200F8F57C /* LocationsListViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };