Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Closes #265] Injection of viewmodel when @Observable not working properly #266

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Sources/Factory/Factory/Injections.swift
Original file line number Diff line number Diff line change
@@ -387,6 +387,64 @@ extension InjectedObject {
self._dependency = StateObject<T>(wrappedValue: wrappedValue)
}
}

/// A property wrapper that injects an observable dependency into a SwiftUI view.
///
/// `InjectedObservable` is designed to automatically resolve and inject dependencies
/// from a shared container, allowing for easy management of observable objects within
/// SwiftUI views. This property wrapper ensures that the dependency is resolved at
/// initialization and provides both direct access and binding capabilities.
///
/// - Note: This property wrapper is available on iOS 17.0, macOS 14.0, tvOS 17.0, and watchOS 10.0.
/// - Requires: The wrapped type `T` must conform to the `Observable` protocol.
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
@MainActor @frozen @propertyWrapper public struct InjectedObservable<T>: DynamicProperty where T: Observable {

/// The observable dependency managed by this property wrapper.
@State fileprivate var dependency: T

/**
Initializes the `InjectedObservable` property wrapper, resolving the dependency from the default container.

- Parameter keyPath: A key path to a `Factory` on the default `Container` that resolves the dependency.

**Example Usage:**
```swift
@InjectedObservable(\.contentViewModel) var viewModel: ContentViewModel
```
*/
public init(_ keyPath: KeyPath<Container, Factory<T>>) {
self._dependency = State<T>(wrappedValue: Container.shared[keyPath: keyPath]())
}

/// Initializes the property wrapper. The dependency is resolved on initialization.
/// - Parameter keyPath: KeyPath to a Factory on the specified Container.
public init<C: SharedContainer>(_ keyPath: KeyPath<C, Factory<T>>) {
self._dependency = State<T>(wrappedValue: C.shared[keyPath: keyPath]())
}

/// Provides direct access to the wrapped observable dependency.
public var wrappedValue: T {
get { dependency }
}

/// Provides a binding to the wrapped observable dependency, allowing for dynamic updates.
public var projectedValue: Binding<T> {
return $dependency
}
}

@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
extension InjectedObservable {
/// Simple initializer with passed parameter bypassing injection.
///
/// Still has issue with attempting to pass dependency into existing view when existing InjectedObject has keyPath.
/// https://forums.swift.org/t/allow-property-wrappers-with-multiple-arguments-to-defer-initialization-when-wrappedvalue-is-not-specified
public init(_ wrappedValue: T) {
self._dependency = State<T>(wrappedValue: wrappedValue)
}
}

#endif

/// Boxed wrapper to provide a Factory when asked
40 changes: 40 additions & 0 deletions Tests/FactoryTests/FactoryInjectionTests.swift
Original file line number Diff line number Diff line change
@@ -294,6 +294,26 @@ final class FactoryInjectionTests: XCTestCase {
let projected = i3.projectedValue
XCTAssertNotNil(projected)
}

@available(iOS 17, *)
@MainActor
func testInjectedObservable() throws {
// Test initializer for default container
let i1 = InjectedObservable(\.contentObservableViewModel)
let cvm1 = i1.wrappedValue
XCTAssertEqual(cvm1.text, "Test")
// Test initializer for custom container
let i2 = InjectedObservable(\CustomContainer.contentObservableViewModel)
let cvm2 = i2.wrappedValue
XCTAssertEqual(cvm2.text, "Test")
// Test initializer for passed parameter
let i3 = InjectedObservable(ContentObservableViewModel())
let cvm3 = i3.wrappedValue
XCTAssertEqual(cvm3.text, "Test")
// Test projected value
let projected = i3.projectedValue
XCTAssertNotNil(projected)
}
#endif

}
@@ -324,5 +344,25 @@ class ResolvingViewModel: ObservableObject {

extension Container: Resolving {}

@available(iOS 17, *)
@Observable
class ContentObservableViewModel {
var text = "Test"
}

@available(iOS 17, *)
extension Container {
var contentObservableViewModel: Factory<ContentObservableViewModel> {
self { ContentObservableViewModel() }
}
}

@available(iOS 17, *)
extension CustomContainer {
var contentObservableViewModel: Factory<ContentObservableViewModel> {
self { ContentObservableViewModel() }
}
}

#endif