[Feature] Dependencies (#3)

This PR contains the work done to provide a simple dependency injection feature whenever required.

To provide further details about this work:
- [x] renamed the `Coordinator` target in the `Package` file as `Coordination`;
- [x] declared the `Dependencies` target in the `Package` file;
- [x] defined the `DependencyKey` public protocol;
- [x] implemented the `DependencyService` public service;
- [x] implemented the `Dependency` public property wrapper;

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: #3
This commit is contained in:
Javier Cicchelli 2023-04-16 15:36:07 +00:00
parent 12add7bf30
commit 9c89a59d1d
15 changed files with 274 additions and 17 deletions

View File

@ -14,15 +14,16 @@ let package = Package(
.library( .library(
name: "SwiftLibs", name: "SwiftLibs",
targets: [ targets: [
"Coordinator", "Coordination",
"Core" "Core",
"Dependencies"
] ]
), ),
], ],
dependencies: [], dependencies: [],
targets: [ targets: [
.target( .target(
name: "Coordinator", name: "Coordination",
dependencies: [], dependencies: [],
exclude: excludePlatforms exclude: excludePlatforms
), ),
@ -30,12 +31,16 @@ let package = Package(
name: "Core", name: "Core",
dependencies: [] dependencies: []
), ),
.target(
name: "Dependencies",
dependencies: []
),
.testTarget( .testTarget(
name: "CoordinatorTests", name: "CoordinationTests",
dependencies: [ dependencies: [
"Coordinator" "Coordination"
], ],
path: "Tests/Coordinator", path: "Tests/Coordination",
exclude: excludePlatforms exclude: excludePlatforms
), ),
.testTarget( .testTarget(
@ -45,6 +50,13 @@ let package = Package(
], ],
path: "Tests/Core" path: "Tests/Core"
), ),
.testTarget(
name: "DependenciesTests",
dependencies: [
"Dependencies"
],
path: "Tests/Dependencies"
),
] ]
) )

View File

@ -1,6 +1,6 @@
// //
// BaseNavigationRouter.swift // BaseNavigationRouter.swift
// Coordinator // Coordination
// //
// Created by Javier Cicchelli on 11/04/2023. // Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved. // Copyright © 2023 Röck+Cöde. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// ModalNavigationRouter.swift // ModalNavigationRouter.swift
// Coordinator // Coordination
// //
// Created by Javier Cicchelli on 11/04/2023. // Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved. // Copyright © 2023 Röck+Cöde. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// PushNavigationRouter.swift // PushNavigationRouter.swift
// Coordinator // Coordination
// //
// Created by Javier Cicchelli on 11/04/2023. // Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved. // Copyright © 2023 Röck+Cöde. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// WindowRouter.swift // WindowRouter.swift
// Coordinator // Coordination
// //
// Created by Javier Cicchelli on 11/04/2023. // Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved. // Copyright © 2023 Röck+Cöde. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// Coordinator.swift // Coordinator.swift
// Coordinator // Coordination
// //
// Created by Javier Cicchelli on 11/04/2023. // Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved. // Copyright © 2023 Röck+Cöde. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// Router.swift // Router.swift
// Coordinator // Coordination
// //
// Created by Javier Cicchelli on 11/04/2023. // Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved. // Copyright © 2023 Röck+Cöde. All rights reserved.
@ -13,7 +13,7 @@ import UIKit
/// This protocol defines how view controllers will be shown and dismissed. /// This protocol defines how view controllers will be shown and dismissed.
public protocol Router: AnyObject { public protocol Router: AnyObject {
// MARK: Typealiases // MARK: Type aliases
typealias OnDismissedClosure = () -> Void typealias OnDismissedClosure = () -> Void

View File

@ -0,0 +1,31 @@
//
// Dependency.swift
// Dependencies
//
// Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved.
//
/// This property wrapper provides a direct connection to the `DependencyService` service.
@propertyWrapper
public struct Dependency<D> {
// MARK: Properties
private let keyPath: WritableKeyPath<DependencyService, D>
/// This property allows direct read/write access to a defined dependency attached to a selected key path.
public var wrappedValue: D {
get { DependencyService[keyPath] }
set { DependencyService[keyPath] = newValue }
}
// MARK: Initialisers
/// Initialise the property wrapper by setting a key path to a defined dependency.
/// - Parameter keyPath: A key path to a defined dependency in the `DependencyService` service.
public init(_ keyPath: WritableKeyPath<DependencyService, D>) {
self.keyPath = keyPath
}
}

View File

@ -0,0 +1,22 @@
//
// DependencyKey.swift
// Dependencies
//
// Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved.
//
/// This protocol defines a key to use in the dependency service.
public protocol DependencyKey {
// MARK: Associated types
/// The associated type representing the type of the dependency key's value.
associatedtype Value
// MARK: Properties
/// The default value for the dependency key.
static var currentValue: Value { get set }
}

View File

@ -0,0 +1,28 @@
//
// DependencyService.swift
// Dependencies
//
// Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved.
//
/// This service provide write/read access to the injected dependencies.
public struct DependencyService {
// MARK: Properties
private static var current = DependencyService()
// MARK: Subscripts
public static subscript<DK: DependencyKey>(key: DK.Type) -> DK.Value {
get { key.currentValue }
set { key.currentValue = newValue }
}
public static subscript<D>(_ keyPath: WritableKeyPath<DependencyService, D>) -> D {
get { current[keyPath: keyPath] }
set { current[keyPath: keyPath] = newValue }
}
}

View File

@ -1,12 +1,12 @@
// //
// TestCoordinators.swift // TestCoordinators.swift
// CoordinatorTests // CoordinationTests
// //
// Created by Javier Cicchelli on 11/04/2023. // Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved. // Copyright © 2023 Röck+Cöde. All rights reserved.
// //
import Coordinator import Coordination
import UIKit import UIKit
// MARK: - Test coordinators // MARK: - Test coordinators

View File

@ -1,12 +1,12 @@
// //
// CoordinatorTests.swift // CoordinatorTests.swift
// CoordinatorTests // CoordinationTests
// //
// Created by Javier Cicchelli on 11/04/2023. // Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved. // Copyright © 2023 Röck+Cöde. All rights reserved.
// //
import Coordinator import Coordination
import UIKit import UIKit
import XCTest import XCTest

View File

@ -0,0 +1,34 @@
//
// TestServices.swift
// DependenciesTests
//
// Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved.
//
import Dependencies
// MARK: - Protocols
protocol TestService {}
// MARK: - Services
struct SomeService: TestService, Equatable {}
struct SomeOtherService: TestService, Equatable {}
// MARK: - DependencyKey
struct TestServiceKey: DependencyKey {
static var currentValue: TestService = SomeService()
}
// MARK: - DependencyService+Keys
extension DependencyService {
var testService: TestService {
get { Self[TestServiceKey.self] }
set { Self[TestServiceKey.self] = newValue }
}
}

View File

@ -0,0 +1,73 @@
//
// DependencyTests.swift
// DependenciesTests
//
// Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved.
//
import Dependencies
import XCTest
final class DependencyTests: XCTestCase {
// MARK: Properties
private var subject: TestSubject!
// MARK: Setup
override func setUp() {
DependencyService[\.testService] = SomeService()
}
// MARK: Tests
func test_readTestService() {
// GIVEN
subject = .init()
// WHEN
let service = subject.testService
// THEN
XCTAssertNotNil(service)
XCTAssert(service is SomeService)
}
func test_writeDependencyKey() async throws {
// GIVEN
subject = .init()
subject.testService = SomeOtherService()
// WHEN
let service = DependencyService[\.testService]
// THEN
XCTAssertNotNil(service)
XCTAssert(service is SomeOtherService)
}
func test_writeDependencyKeyTwice() async throws {
// GIVEN
subject = .init()
subject.testService = SomeOtherService()
subject.testService = SomeService()
// WHEN
let service = DependencyService[\.testService]
// THEN
XCTAssertNotNil(service)
XCTAssert(service is SomeService)
}
}
// MARK: - TestSubject
private struct TestSubject {
@Dependency(\.testService) var testService
}

View File

@ -0,0 +1,57 @@
//
// DependencyServiceTests.swift
// DependenciesTests
//
// Created by Javier Cicchelli on 11/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved.
//
import Dependencies
import XCTest
final class DependencyServiceTests: XCTestCase {
// MARK: Setup
override func setUp() {
DependencyService[\.testService] = SomeService()
}
// MARK: Tests
func test_readDependencyKey() async throws {
// GIVEN
// WHEN
let service = DependencyService[\.testService]
// THEN
XCTAssertNotNil(service)
XCTAssert(service is SomeService)
}
func test_writeDependencyKey() async throws {
// GIVEN
DependencyService[\.testService] = SomeOtherService()
// WHEN
let service = DependencyService[\.testService]
// THEN
XCTAssertNotNil(service)
XCTAssert(service is SomeOtherService)
}
func test_writeDependencyKeyTwice() async throws {
// GIVEN
DependencyService[\.testService] = SomeOtherService()
DependencyService[\.testService] = SomeService()
// WHEN
let service = DependencyService[\.testService]
// THEN
XCTAssertNotNil(service)
XCTAssert(service is SomeService)
}
}