diff --git a/README.md b/README.md index 315ecd7..36c88e1 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ AppState is a Swift Package that simplifies the management of application state ### Dependency Management - **Dependency:** Struct for encapsulating dependencies within the app's scope. +- 🍎 **ObservedDependency:** Struct for encapsulating dependencies within the app's scope. Backed by an `@ObservedObject` to publish changes to SwiftUI views. - **Scope:** Represents a specific context within an app, defined by a unique name and ID. ### Property Wrappers @@ -300,7 +301,7 @@ In this case, ContentView has access to the networkService dependency and can us ### Using Dependency with ObservableObject -When your dependency is an `ObservableObject`, any changes to it will automatically update your SwiftUI views. Make sure your service conforms to the `ObservableObject` protocol. To do this, you should not use the `@AppDependency` property wrapper, but instead use the `@ObservedObject` property wrapper. +When your dependency is an `ObservableObject`, any changes to it will automatically update your SwiftUI views. Make sure your service conforms to the `ObservableObject` protocol. To do this, you should not use the `@AppDependency` property wrapper, but instead use the `@ObservedDependency` property wrapper. Here's an example: @@ -318,7 +319,7 @@ extension Application { } struct ContentView: View { - @ObservedObject var dataService = Application.dependency(\.dataService) + @ObservedDependency(\.dataService) private var dataService var body: some View { List(dataService.data, id: \.self) { item in diff --git a/Sources/AppState/PropertyWrappers/Dependency/ObservedDependency.swift b/Sources/AppState/PropertyWrappers/Dependency/ObservedDependency.swift new file mode 100644 index 0000000..53d056d --- /dev/null +++ b/Sources/AppState/PropertyWrappers/Dependency/ObservedDependency.swift @@ -0,0 +1,49 @@ +#if !os(Linux) && !os(Windows) +import SwiftUI + +/// The `@ObservedDependency` property wrapper is a feature provided by AppState, intended to simplify dependency handling throughout your application. It makes it easy to access, share, and manage dependencies in a neat and Swift idiomatic way. It works the same as `@AppDependency`, but comes with the power of the `@ObservedObject` property wrapper. +@propertyWrapper public struct ObservedDependency: DynamicProperty where Value: ObservableObject { + /// Path for accessing `ObservedDependency` from Application. + private let keyPath: KeyPath> + + @ObservedObject private var observedObject: Value + + private let fileID: StaticString + private let function: StaticString + private let line: Int + private let column: Int + + /// Represents the current value of the `ObservedDependency`. + public var wrappedValue: Value { observedObject } + + /// A binding to the `ObservedDependency`'s value, which can be used with SwiftUI views. + public var projectedValue: ObservedObject.Wrapper { $observedObject } + + /** + Initializes the AppDependency with a `keyPath` for accessing `Dependency` in Application. + + - Parameter keyPath: The `KeyPath` for accessing `Dependency` in Application. + */ + public init( + _ keyPath: KeyPath>, + _ fileID: StaticString = #fileID, + _ function: StaticString = #function, + _ line: Int = #line, + _ column: Int = #column + ) { + self.keyPath = keyPath + self.fileID = fileID + self.function = function + self.line = line + self.column = column + + self.observedObject = Application.dependency( + keyPath, + fileID, + function, + line, + column + ) + } +} +#endif diff --git a/Tests/AppStateTests/ObservedDependencyTests.swift b/Tests/AppStateTests/ObservedDependencyTests.swift new file mode 100644 index 0000000..ed96dc3 --- /dev/null +++ b/Tests/AppStateTests/ObservedDependencyTests.swift @@ -0,0 +1,53 @@ +#if !os(Linux) && !os(Windows) +import SwiftUI +import XCTest +@testable import AppState + +fileprivate class ObservableService: ObservableObject { + @Published var count: Int + + init() { + count = 0 + } +} + +fileprivate extension Application { + var test: Dependency { + dependency("!!!") + } + + var observableService: Dependency { + dependency(ObservableService()) + } +} + +fileprivate struct ExampleDependencyWrapper { + @ObservedDependency(\.observableService) var service + + func test() { + service.count += 1 + + _ = Picker("", selection: $service.count, content: EmptyView.init) + } +} + +final class ObservedDependencyTests: XCTestCase { + override class func setUp() { + Application.logging(isEnabled: true) + } + + override class func tearDown() { + Application.logger.debug("ObservedDependencyTests \(Application.description)") + } + + func testDependency() { + let example = ExampleDependencyWrapper() + + XCTAssertEqual(example.service.count, 0) + + example.test() + + XCTAssertEqual(example.service.count, 1) + } +} +#endif