From b74795a3051b690a3280cc6297d4538fd2aacc03 Mon Sep 17 00:00:00 2001 From: Fabian Zwick Date: Fri, 16 Aug 2024 13:52:29 +0200 Subject: [PATCH] Add @DynamicInjected property wrapper --- .../Factory.docc/Additional/Migration.md | 2 +- .../Factory/Factory.docc/Advanced/Design.md | 2 +- .../Factory/Factory.docc/Basics/Containers.md | 2 +- .../Factory.docc/Basics/Resolutions.md | 2 +- Sources/Factory/Factory/Injections.swift | 44 +++++++++++++++++++ .../FactoryTests/FactoryInjectionTests.swift | 23 ++++++++++ 6 files changed, 71 insertions(+), 4 deletions(-) diff --git a/Sources/Factory/Factory.docc/Additional/Migration.md b/Sources/Factory/Factory.docc/Additional/Migration.md index 4b940cf8..d84fb9d7 100644 --- a/Sources/Factory/Factory.docc/Additional/Migration.md +++ b/Sources/Factory/Factory.docc/Additional/Migration.md @@ -97,7 +97,7 @@ Finally, you can also use the @Injected property wrapper. That's changed too, an ``` The @Injected property wrapper looks for dependencies in the shared container, so this example is functionally identical to the `Container.shared.service()` version shown above. -See ``Injected``, ``LazyInjected``, ``WeakLazyInjected``, and ``InjectedObject`` for more. +See ``Injected``, ``LazyInjected``, ``WeakLazyInjected``, ``DynamicInjected`` and ``InjectedObject`` for more. ```swift // Factory 1.0 version for reference diff --git a/Sources/Factory/Factory.docc/Advanced/Design.md b/Sources/Factory/Factory.docc/Advanced/Design.md index 376ec0bc..3b508ccd 100644 --- a/Sources/Factory/Factory.docc/Advanced/Design.md +++ b/Sources/Factory/Factory.docc/Advanced/Design.md @@ -146,7 +146,7 @@ class ContentViewModel: ObservableObject { ``` Factory 2.0 also updates the syntax to use keyPaths, much like SwiftUI environment variables. -See ``Injected``, ``LazyInjected``, ``WeakLazyInjected``, and ``InjectedObject`` for more. +See ``Injected``, ``LazyInjected``, ``WeakLazyInjected``, ``DynamicInjected`` and ``InjectedObject`` for more. ## Breaking Changes diff --git a/Sources/Factory/Factory.docc/Basics/Containers.md b/Sources/Factory/Factory.docc/Basics/Containers.md index d5fac99c..196554d9 100644 --- a/Sources/Factory/Factory.docc/Basics/Containers.md +++ b/Sources/Factory/Factory.docc/Basics/Containers.md @@ -136,7 +136,7 @@ class ContentViewModel: ObservableObject { ``` We now have an instance of `cachedService` in our view model, as well as a reference to the same instance cached in `MyContainer.shared.manager`. -See ``Injected``, ``LazyInjected``, ``WeakLazyInjected``, and ``InjectedObject`` for more. +See ``Injected``, ``LazyInjected``, ``WeakLazyInjected``, ``DynamicInjected`` and ``InjectedObject`` for more. ## Registration and Scope Management diff --git a/Sources/Factory/Factory.docc/Basics/Resolutions.md b/Sources/Factory/Factory.docc/Basics/Resolutions.md index 2c44ce47..ce6b8b69 100644 --- a/Sources/Factory/Factory.docc/Basics/Resolutions.md +++ b/Sources/Factory/Factory.docc/Basics/Resolutions.md @@ -69,7 +69,7 @@ struct ContentView: View { } } ``` -See ``Injected``, ``LazyInjected``, ``WeakLazyInjected``, and ``InjectedObject`` for more. +See ``Injected``, ``LazyInjected``, ``WeakLazyInjected``, ``DynamicInjected`` and ``InjectedObject`` for more. ### Global Keypath Resolution from Shared Container Factory provides two global functions that utilize keypaths for resolution. diff --git a/Sources/Factory/Factory/Injections.swift b/Sources/Factory/Factory/Injections.swift index 8aee3b1c..258ced15 100644 --- a/Sources/Factory/Factory/Injections.swift +++ b/Sources/Factory/Factory/Injections.swift @@ -253,6 +253,50 @@ import SwiftUI } +/// Convenience property wrapper takes a factory and resolves an instance of the desired type. +/// +/// Property wrappers implement an annotation pattern to resolving dependencies, similar to using +/// EnvironmentObject in SwiftUI. +/// ```swift +/// class MyViewModel { +/// @DynamicInjected(\.myService) var service +/// @DynamicInjected(\MyCustomContainer.myService) var service +/// } +/// ``` +/// The provided keypath resolves to a Factory definition on the `shared` container required for each Container type. +/// The short version of the keyPath resolves to the default container, while the expanded version +/// allows you to point an instance on your own customer container type. +/// +/// - Note: The @DynamicInjected property wrapper will be resolved on **access**. In the above example +/// the referenced dependencies will be acquired each time the property is accessed. +@propertyWrapper public struct DynamicInjected { + + private let reference: BoxedFactoryReference + + /// Initializes the property wrapper. The dependency is resolved on access. + /// - Parameter keyPath: KeyPath to a Factory on the default Container. + public init(_ keyPath: KeyPath>) { + self.reference = FactoryReference(keypath: keyPath) + } + + /// Initializes the property wrapper. The dependency is resolved on access. + /// - Parameter keyPath: KeyPath to a Factory on the specified Container. + public init(_ keyPath: KeyPath>) { + self.reference = FactoryReference(keypath: keyPath) + } + + /// Manages the wrapped dependency. + public var wrappedValue: T { + get { return reference.factory().resolve() } + } + + /// Unwraps the property wrapper granting access to the resolve/reset function. + public var projectedValue: Factory { + get { return reference.factory() } + } +} + + /// Basic property wrapper for optional injected types @propertyWrapper public struct InjectedType { private var dependency: T? diff --git a/Tests/FactoryTests/FactoryInjectionTests.swift b/Tests/FactoryTests/FactoryInjectionTests.swift index c27bd7c7..be723f33 100644 --- a/Tests/FactoryTests/FactoryInjectionTests.swift +++ b/Tests/FactoryTests/FactoryInjectionTests.swift @@ -27,6 +27,13 @@ class Services3 { init() {} } +class Services4 { + @DynamicInjected(\.myServiceType) var service + @DynamicInjected(\.mockService) var mock + @DynamicInjected(\CustomContainer.test) var test + init() {} +} + class Services5 { @Injected(\.optionalService) var service init() {} @@ -55,6 +62,7 @@ extension Container { fileprivate var services1: Factory { self { Services1() } } fileprivate var services2: Factory { self { Services2() } } fileprivate var services3: Factory { self { Services3() } } + fileprivate var services4: Factory { self { Services4() } } fileprivate var servicesP: Factory { self { ServicesP() }.shared } fileprivate var servicesC: Factory { self { ServicesC() }.shared } } @@ -124,6 +132,13 @@ final class FactoryInjectionTests: XCTestCase { XCTAssertEqual(services.test.text(), "MockService32") XCTAssertNotNil(services.$service.resolvedOrNil()) } + + func testDynamicInjection() throws { + let services = Services4() + XCTAssertEqual(services.service.text(), "MyService") + XCTAssertEqual(services.mock.text(), "MockService") + XCTAssertEqual(services.test.text(), "MockService32") + } func testLazyInjectionOccursOnce() throws { let services = Services2() @@ -233,6 +248,14 @@ final class FactoryInjectionTests: XCTestCase { XCTAssertNil(service.service) } + + func testDynamicInjectionResolve() throws { + let object = Container.shared.services4() + let oldId = object.service.id + // should have new instance + let newId = object.service.id + XCTAssertTrue(oldId != newId) + } #if canImport(SwiftUI) @available(iOS 14, *)