diff --git a/Apps/Locations/Libraries/Sources/Core/Protocols/Application.swift b/Apps/Locations/Libraries/Sources/Core/Protocols/Application.swift new file mode 100644 index 0000000..6f60af7 --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Core/Protocols/Application.swift @@ -0,0 +1,23 @@ +// +// Application.swift +// Core +// +// Created by Javier Cicchelli on 13/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Foundation +import UIKit + +public protocol Application { + + // MARK: Functions + + func canOpenURL(_ url: URL) -> Bool + func open( + _ url: URL, + options: [UIApplication.OpenExternalURLOptionsKey : Any], + completionHandler completion: ((Bool) -> Void)? + ) + +} diff --git a/Apps/Locations/Libraries/Sources/Persistence/Protocols/Service.swift b/Apps/Locations/Libraries/Sources/Persistence/Protocols/Service.swift new file mode 100644 index 0000000..c37618d --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Persistence/Protocols/Service.swift @@ -0,0 +1,36 @@ +// +// Service.swift +// Persistence +// +// Created by Javier Cicchelli on 13/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import CoreData + +public protocol Service { + + // MARK: Properties + + /// The main managed object context. + var viewContext: NSManagedObjectContext { get } + + // MARK: Functions + + /// Create a private queue context. + /// - Returns: A concurrent `NSManagedObjectContext` context instance ready to use. + func makeTaskContext() -> NSManagedObjectContext + + /// Create a child context of the view context. + /// - Returns: A generated child `NSManagedObjectContext` context instance ready to use. + func makeChildContext() -> NSManagedObjectContext + + /// Save a given context, + /// - Parameter context: A `NSManagedObjectContext` context instance to save. + func save(context: NSManagedObjectContext) + + /// Save a given child context as well as its respective parent context. + /// - Parameter context: A child `NSManagedObjectContext` context instance to save. + func save(childContext context: NSManagedObjectContext) + +} diff --git a/Apps/Locations/Libraries/Sources/Persistence/Services/PersistenceService.swift b/Apps/Locations/Libraries/Sources/Persistence/Services/PersistenceService.swift index c81ab12..1fe8550 100644 --- a/Apps/Locations/Libraries/Sources/Persistence/Services/PersistenceService.swift +++ b/Apps/Locations/Libraries/Sources/Persistence/Services/PersistenceService.swift @@ -26,7 +26,7 @@ public struct PersistenceService { else { fatalError("Could not load the model from the library.") } - + container = NSPersistentContainer( name: .Model.name, managedObjectModel: managedObjectModel @@ -35,10 +35,18 @@ public struct PersistenceService { setContainer(inMemory) } - // MARK: Functions +} + +// MARK: - Service + +extension PersistenceService: Service { - /// Create a private queue context. - /// - Returns: A concurrent `NSManagedObjectContext` context instance ready to use. + // MARK: Properties + + public var viewContext: NSManagedObjectContext { container.viewContext } + + // MARK: Functions + public func makeTaskContext() -> NSManagedObjectContext { let taskContext = container.newBackgroundContext() @@ -48,8 +56,6 @@ public struct PersistenceService { return taskContext } - /// Create a child context of the view context. - /// - Returns: A generated child `NSManagedObjectContext` context instance ready to use. public func makeChildContext() -> NSManagedObjectContext { let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) @@ -60,8 +66,6 @@ public struct PersistenceService { return context } - /// Save a given context, - /// - Parameter context: A `NSManagedObjectContext` context instance to save. public func save(context: NSManagedObjectContext) { guard context.hasChanges else { return @@ -75,8 +79,6 @@ public struct PersistenceService { } } - /// Save a given child context as well as its respective parent context. - /// - Parameter context: A child `NSManagedObjectContext` context instance to save. public func save(childContext context: NSManagedObjectContext) { guard context.hasChanges else { return @@ -100,7 +102,7 @@ public struct PersistenceService { fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } - + } // MARK: - Helpers diff --git a/Apps/Locations/Libraries/Sources/Remote/Protocols/Service.swift b/Apps/Locations/Libraries/Sources/Remote/Protocols/Service.swift new file mode 100644 index 0000000..1da2d50 --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Remote/Protocols/Service.swift @@ -0,0 +1,19 @@ +// +// Service.swift +// Remote +// +// Created by Javier Cicchelli on 13/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Foundation + +public protocol Service { + + // MARK: Functions + + /// Retrieve a set of locations. + /// - Returns: The set of locations represented as a `Location` instances. + func getLocations() async throws -> [Location] + +} diff --git a/Apps/Locations/Libraries/Sources/Remote/Services/RemoteService.swift b/Apps/Locations/Libraries/Sources/Remote/Services/RemoteService.swift index b9bca95..ee71790 100644 --- a/Apps/Locations/Libraries/Sources/Remote/Services/RemoteService.swift +++ b/Apps/Locations/Libraries/Sources/Remote/Services/RemoteService.swift @@ -21,6 +21,12 @@ public struct RemoteService { self.client = RemoteClient(configuration: configuration) } +} + +// MARK: - Service + +extension RemoteService: Service { + // MARK: Functions public func getLocations() async throws -> [Location] { @@ -29,7 +35,7 @@ public struct RemoteService { for: Locations.self ).locations } - + } // MARK: - Models diff --git a/Apps/Locations/Sources/Coordinators/LocationsListCoordinator.swift b/Apps/Locations/Sources/Coordinators/LocationsListCoordinator.swift index 05d5ff5..11db617 100644 --- a/Apps/Locations/Sources/Coordinators/LocationsListCoordinator.swift +++ b/Apps/Locations/Sources/Coordinators/LocationsListCoordinator.swift @@ -7,10 +7,15 @@ // import Core +import Dependency import UIKit class LocationsListCoordinator: Coordinator { + // MARK: Dependencies + + @Dependency(\.app) private var app + // MARK: Properties var children: [Coordinator] = [] @@ -62,11 +67,11 @@ extension LocationsListCoordinator: LocationsListCoordination { } func openWikipediaApp(with url: URL) { - guard UIApplication.shared.canOpenURL(url) else { + guard app.canOpenURL(url) else { return } - UIApplication.shared.open(url) + app.open(url, options: [:], completionHandler: nil) } } diff --git a/Apps/Locations/Sources/Extensions/DependencyService+Keys.swift b/Apps/Locations/Sources/Extensions/DependencyService+Keys.swift index b1286d0..0f4a532 100644 --- a/Apps/Locations/Sources/Extensions/DependencyService+Keys.swift +++ b/Apps/Locations/Sources/Extensions/DependencyService+Keys.swift @@ -6,19 +6,26 @@ // Copyright © 2023 Röck+Cöde. All rights reserved. // +import Core import Dependency import Persistence import Remote +import UIKit // MARK: - DependencyService+Keys extension DependencyService { - var persistence: PersistenceService { + var app: Core.Application { + get { Self[ApplicationKey.self] } + set { Self[ApplicationKey.self] = newValue } + } + + var persistence: Persistence.Service { get { Self[PersistenceKey.self] } set { Self[PersistenceKey.self] = newValue } } - var remote: RemoteService { + var remote: Remote.Service { get { Self[RemoteKey.self] } set { Self[RemoteKey.self] = newValue } } @@ -26,10 +33,14 @@ extension DependencyService { // MARK: - Dependency keys +struct ApplicationKey: DependencyKey { + static var currentValue: Core.Application = UIApplication.shared +} + struct PersistenceKey: DependencyKey { - static var currentValue: PersistenceService = .shared + static var currentValue: Persistence.Service = PersistenceService.shared } struct RemoteKey: DependencyKey { - static var currentValue: RemoteService = .init() + static var currentValue: Remote.Service = RemoteService() } diff --git a/Apps/Locations/Sources/Extensions/UIApplication+Conformances.swift b/Apps/Locations/Sources/Extensions/UIApplication+Conformances.swift new file mode 100644 index 0000000..598333d --- /dev/null +++ b/Apps/Locations/Sources/Extensions/UIApplication+Conformances.swift @@ -0,0 +1,12 @@ +// +// UIApplication+Conformances.swift +// Locations +// +// Created by Javier Cicchelli on 13/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Core +import UIKit + +extension UIApplication: Application {} diff --git a/Apps/Locations/Sources/Screens/LocationsList/LocationsListViewModel.swift b/Apps/Locations/Sources/Screens/LocationsList/LocationsListViewModel.swift index dedcbdc..90f1dd4 100644 --- a/Apps/Locations/Sources/Screens/LocationsList/LocationsListViewModel.swift +++ b/Apps/Locations/Sources/Screens/LocationsList/LocationsListViewModel.swift @@ -21,7 +21,7 @@ class LocationsListViewModel: ObservableObject { weak var coordinator: LocationsListCoordination? - private lazy var locationProvider = LocationProvider(managedContext: persistence.container.viewContext) + private lazy var locationProvider = LocationProvider(managedContext: persistence.viewContext) @Published private var viewStatus: LocationsListViewStatus = .initialised diff --git a/Apps/Locations/Sources/Use Cases/LoadRemoteLocationsUseCase.swift b/Apps/Locations/Sources/Use Cases/LoadRemoteLocationsUseCase.swift index 50c3cc1..fa6d45c 100644 --- a/Apps/Locations/Sources/Use Cases/LoadRemoteLocationsUseCase.swift +++ b/Apps/Locations/Sources/Use Cases/LoadRemoteLocationsUseCase.swift @@ -15,14 +15,14 @@ struct LoadRemoteLocationsUseCase { // MARK: Properties - private let persistence: PersistenceService - private let remoteService: RemoteService + private let persistence: Persistence.Service + private let remoteService: Remote.Service // MARK: Initialisers init( - persistence: PersistenceService, - remoteService: RemoteService + persistence: Persistence.Service, + remoteService: Remote.Service ) { self.persistence = persistence self.remoteService = remoteService diff --git a/Apps/Locations/Sources/Use Cases/SaveLocalLocationUseCase.swift b/Apps/Locations/Sources/Use Cases/SaveLocalLocationUseCase.swift index 0d2d074..2d078d5 100644 --- a/Apps/Locations/Sources/Use Cases/SaveLocalLocationUseCase.swift +++ b/Apps/Locations/Sources/Use Cases/SaveLocalLocationUseCase.swift @@ -13,11 +13,11 @@ struct SaveLocalLocationUseCase { // MARK: Properties - private let persistence: PersistenceService + private let persistence: Persistence.Service // MARK: Initialisers - init(persistence: PersistenceService) { + init(persistence: Persistence.Service) { self.persistence = persistence } diff --git a/DeepLinking.xcodeproj/project.pbxproj b/DeepLinking.xcodeproj/project.pbxproj index c66b4b3..e66e389 100644 --- a/DeepLinking.xcodeproj/project.pbxproj +++ b/DeepLinking.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 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 */; }; + 46DF736D29E82A1500AA6D21 /* UIApplication+Conformances.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DF736C29E82A1500AA6D21 /* UIApplication+Conformances.swift */; }; 46EB331B29E1CE04001D5EAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB331A29E1CE04001D5EAF /* AppDelegate.swift */; }; 46EB331F29E1CE04001D5EAF /* LocationsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB331E29E1CE04001D5EAF /* LocationsListViewController.swift */; }; 46EB332729E1CE05001D5EAF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 46EB332629E1CE05001D5EAF /* Assets.xcassets */; }; @@ -143,6 +144,7 @@ 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 = ""; }; + 46DF736C29E82A1500AA6D21 /* UIApplication+Conformances.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Conformances.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 = ""; }; @@ -180,6 +182,7 @@ children = ( 02031EC829E60B29003C108C /* DependencyService+Keys.swift */, 02031F0929E7645F003C108C /* Location+URLs.swift */, + 46DF736C29E82A1500AA6D21 /* UIApplication+Conformances.swift */, ); path = Extensions; sourceTree = ""; @@ -550,6 +553,7 @@ 02031EEA29E6B495003C108C /* ErrorMessageView.swift in Sources */, 46C3B7DC29E5ED2300F8F57C /* LocationsAddCoordination.swift in Sources */, 46C3B7D829E5E55000F8F57C /* LocationsListCoordination.swift in Sources */, + 46DF736D29E82A1500AA6D21 /* UIApplication+Conformances.swift in Sources */, 46C3B7D629E5E50500F8F57C /* LocationsListViewModeling.swift in Sources */, 4656CBC229E6D33C00600EE6 /* LoadRemoteLocationsUseCase.swift in Sources */, 46C3B7CF29E5D00E00F8F57C /* LocationsAddViewModel.swift in Sources */, diff --git a/README.md b/README.md index 3c6382f..0f69f81 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The ultimate purpose of this application is to open the **Wikipedia** app in the -Of course, to accomplish such goal the app therefore shows a list of locations (which are either fetched from a remote server or created by the user) and also, allows the user to add new location coordinates to this list. +Of course, to accomplish such goal the app therefore shows a list of locations (which are either fetched from a remote server or created by the user) and also, allows the user to add new location coordinates to this list by selecting them from a map. ## Features @@ -33,12 +33,12 @@ While the **Wikipedia** app does: ## Implementation -This application was built as a `UIKit` application given that the [assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) explicitedly indicates the app should target the `iOS` platform (**iPhone** app only, in this particular case) and also, because it disallow the use of the `SwiftUI` framework. It is out of discussion that the `UIKit` framework has been battle-tested for over a decade now and [Apple](https://apple.com) keeps updating and adding features to it on a regular basis. The imperative nature of the framework, which is based in implementing how the UI components for `iOS` platform should work, is perfectly suitable the developer who want absolute control over the UI, at a cost of maintaining platform-specific, more complex codebases. +This application was built as a `UIKit` application given that the [assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) explicitedly indicates the app should target the `iOS` platform (**iPhone** app only, in this particular case) and also, because it disallow the use of the `SwiftUI` framework. It is out of discussion that the `UIKit` framework has been battle-tested for over a decade now and [Apple](https://apple.com) keeps updating and adding features to it on a regular basis. The imperative nature of the framework, which is based in implementing how the `iOS` platform UI components should work, is perfectly suitable the developer who want absolute control over the UI, at a cost of maintaining platform-specific, more complex codebases. -With regards to the choice of framework to built this app, it also comes the question of the type of architecture pattern to use in it: for this particular case, and given the limitations of how the view controllers have been defined in the Apple platforms, I chosed to define a [MVVM](https://en.wikipedia.org/wiki/Model–view–viewmodel)-C that facilitates the separation between logic and UI compoments while decoupling them from the navigation logic by using coordinates (and routers). +With regards to the choice of framework to built this app, it also comes the question of the type of architecture pattern to use in it: for this particular case, and given the limitations of how the view controllers have been defined in the Apple platforms, [MVVM](https://en.wikipedia.org/wiki/Model–view–viewmodel)-C is the chosen architecrture as it facilitates the separation between logic and UI components while also, decoupling the navigation logic by using coordinates (and routers) from them. -Now that design patterns have been mentioned, in this exercise some well-known patterns are being used in some degree. For example, the *Singleton* pattern is used to initialise the `PersistenceService` service in the `Persistence` library. Both public and internal *Interfaces* that either describe an object or how it should behave are used throughout this codebase, as this pattern is essential to create decoupled components that can be easily plugged as dependencies whenever needed because it forces the developer to think about (single) responsibilites and, as a consequence, these components are also easy to test in isolation as developer can easily create mocks, stubs and spies out of them. Last, but definitely not least, the *Use cases* are a pattern from Android that basically execute a function based on some given input, and provides an output after that particular function is finished. This pattern is particularly useful to encapsulate some certain logic that a view model needs to execute in a simple way. +Now that design patterns have been mentioned, in this exercise some well-known patterns are being used in some degree. For example, the *Singleton* pattern is used to initialise the `PersistenceService` service in the `Persistence` library. Both public and internal *Interfaces* that either describe an entity or how the entity should behave are used throughout this codebase, as this pattern is essential to create decoupled components that can be easily plugged as dependencies whenever needed as this forces the developer to think about (single) responsibilites and, as a consequence, these components are also easy to test in isolation as mocks, stubs and spies can be easily created out of them. Last, but definitely not least, the *Use cases* are a pattern from Android that basically execute a function based on some given input, and provides an output after that particular function is finished. This pattern is particularly useful to encapsulate in a simple way some certain logic from view models. -This application was built with scalability in terms of the codebase in mind, which tries to address how this codebase could grow in a controllable, organised manner. For this very reason, this application uses the [Swift Package Manager](https://www.swift.org/package-manager/) (or simply `SwiftPM`) to define the `Core`, `Dependency`, `Persistence` and `Remote` libraries of the `Library` package, that the **Locations** target should use extensively. These libraries focus on a specific purpose, and they can be self-contained, like in the case of the `Persistence` library that contains its own **CoreData** data model definitons and respective assets inside. Packages could use 3rd party dependencies if needed. This approach forces the developer to think about actual separation of concerns, as the different dependencies are grouped as independent, reusable building blocks, and to move the code into the SPM packages out of the main app target, reducing compiling time and overal weight of the application. +This application was built with scalability in terms of the codebase in mind, which tries to address how this codebase could grow in a controllable, organised manner. For this very reason, this application uses the [Swift Package Manager](https://www.swift.org/package-manager/) (or simply **SwiftPM**) to define the `Core`, `Dependency`, `Persistence` and `Remote` libraries of the `Library` package, that the **Locations** target should use extensively. These libraries focus on a specific purpose, and they can be self-contained, like in the case of the `Persistence` library that contains its own **CoreData** data model definitons and respective assets inside. Packages could use 3rd party dependencies if needed. This approach forces the developer to think about actual separation of concerns, as the different dependencies are grouped as independent, reusable building blocks, and to move the code into the SPM packages out of the main app target, reducing compiling time and overall weight of the application. As a (indirect) consequence for the use of **SwiftPM** packages, the [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) pattern comes into question. I implemented my own simple **DI** mechanism that uses extensively the dynamic property wrapper functionality in the last versions of the [Swift](https://www.swift.org) language rather than using a 3rd party dependency for this case.