Skip to content

Commit

Permalink
Merge pull request #220 from FKREISE/dynamicinjected
Browse files Browse the repository at this point in the history
Add @DynamicInjected property wrapper
  • Loading branch information
hmlongco authored Oct 10, 2024
2 parents c342b93 + b74795a commit dbed962
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Sources/Factory/Factory.docc/Additional/Migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/Factory/Factory.docc/Advanced/Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Sources/Factory/Factory.docc/Basics/Containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Sources/Factory/Factory.docc/Basics/Resolutions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
44 changes: 44 additions & 0 deletions Sources/Factory/Factory/Injections.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {

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<Container, Factory<T>>) {
self.reference = FactoryReference<Container, T>(keypath: keyPath)
}

/// Initializes the property wrapper. The dependency is resolved on access.
/// - Parameter keyPath: KeyPath to a Factory on the specified Container.
public init<C: SharedContainer>(_ keyPath: KeyPath<C, Factory<T>>) {
self.reference = FactoryReference<C, T>(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<T> {
get { return reference.factory() }
}
}


/// Basic property wrapper for optional injected types
@propertyWrapper public struct InjectedType<T> {
private var dependency: T?
Expand Down
23 changes: 23 additions & 0 deletions Tests/FactoryTests/FactoryInjectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand Down Expand Up @@ -55,6 +62,7 @@ extension Container {
fileprivate var services1: Factory<Services1> { self { Services1() } }
fileprivate var services2: Factory<Services2> { self { Services2() } }
fileprivate var services3: Factory<Services3> { self { Services3() } }
fileprivate var services4: Factory<Services4> { self { Services4() } }
fileprivate var servicesP: Factory<ServicesP> { self { ServicesP() }.shared }
fileprivate var servicesC: Factory<ServicesC> { self { ServicesC() }.shared }
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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, *)
Expand Down

0 comments on commit dbed962

Please sign in to comment.