From 6b31bac1a11fcc1e8213e8f7d818312e55a033ef Mon Sep 17 00:00:00 2001 From: maximkrouk Date: Tue, 21 Dec 2021 16:35:34 +0300 Subject: [PATCH 1/2] Expose ComposableEnvironment.updatingFromParentIfNeeded to public API --- Sources/ComposableEnvironment/ComposableEnvironment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComposableEnvironment/ComposableEnvironment.swift b/Sources/ComposableEnvironment/ComposableEnvironment.swift index 1fbdab5..ff698ec 100644 --- a/Sources/ComposableEnvironment/ComposableEnvironment.swift +++ b/Sources/ComposableEnvironment/ComposableEnvironment.swift @@ -46,7 +46,7 @@ open class ComposableEnvironment { var upToDateDerivedEnvironments: NSHashTable = .weakObjects() @discardableResult - func updatingFromParentIfNeeded(_ parent: ComposableEnvironment) -> Self { + public func updatingFromParentIfNeeded(_ parent: ComposableEnvironment) -> Self { if !parent.upToDateDerivedEnvironments.contains(self) { // The following line updates the `environment`'s dependencies, invalidating its children // dependencies when it mutates its own `dependencies` property as a side effect. From f94eb517383e2c2b08976f5e5ab60f6f7ff46b81 Mon Sep 17 00:00:00 2001 From: maximkrouk Date: Fri, 24 Dec 2021 17:54:12 +0300 Subject: [PATCH 2/2] Implement ComposableDependencies as a graph --- .gitignore | 1 + .../contents.xcworkspacedata | 7 -- .../xcschemes/ComposableEnvironment.xcscheme | 77 ------------------- .../ComposableEnvironment.swift | 24 ++++-- .../ComposableEnvironment/Dependencies.swift | 10 +++ .../ComposableEnvironment/Dependency.swift | 57 ++++++++++++++ .../DerivedEnvironment.swift | 51 ++++++++++++ .../Reducer+ComposableEnvironment.swift | 8 +- .../ComposableEnvironmentTests.swift | 64 +++++++-------- .../ReducerComposableEnvironmentTests.swift | 8 +- 10 files changed, 175 insertions(+), 132 deletions(-) delete mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/ComposableEnvironment.xcscheme diff --git a/.gitignore b/.gitignore index 57e7f7f..78abd26 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /*.xcodeproj xcuserdata/ Package.resolved +.swiftpm diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ComposableEnvironment.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ComposableEnvironment.xcscheme deleted file mode 100644 index 929f38a..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/ComposableEnvironment.xcscheme +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/ComposableEnvironment/ComposableEnvironment.swift b/Sources/ComposableEnvironment/ComposableEnvironment.swift index ff698ec..02bc1e8 100644 --- a/Sources/ComposableEnvironment/ComposableEnvironment.swift +++ b/Sources/ComposableEnvironment/ComposableEnvironment.swift @@ -35,7 +35,15 @@ open class ComposableEnvironment { /// individual dependencies. These values ill propagate to each child``DerivedEnvironment`` as /// well as their own children ``DerivedEnvironment``. public required init() {} - + + var _dependencies = _ComposableDependencies() + + @discardableResult + func connected(to env: ComposableEnvironment) -> Self { + self._dependencies.context = env._dependencies + return self + } + var dependencies: ComposableDependencies = .init() { didSet { // This will make any child refetch its upstream dependencies when accessed. @@ -77,23 +85,23 @@ open class ComposableEnvironment { /// .with(\.mainQueue, .main) /// ``` @discardableResult - public func with(_ keyPath: WritableKeyPath, _ value: V) -> Self { - dependencies[keyPath: keyPath] = value + public func with(_ keyPath: WritableKeyPath<_ComposableDependencies, V>, _ value: V) -> Self { + _dependencies[keyPath: keyPath] = value return self } /// A read-write subcript to directly access a dependency from its `KeyPath` in /// ``ComposableDependencies``. - public subscript(keyPath: WritableKeyPath) -> Value { - get { dependencies[keyPath: keyPath] } - set { dependencies[keyPath: keyPath] = newValue } + public subscript(keyPath: WritableKeyPath<_ComposableDependencies, Value>) -> Value { + get { _dependencies[keyPath: keyPath] } + set { _dependencies[keyPath: keyPath] = newValue } } /// A read-only subcript to directly access a dependency from ``ComposableDependencies``. /// - Remark: This direct access can't be used to set a dependency, as it will try to go through /// the setter part of a ``Dependency`` property wrapper, which is not allowed yet. You can use /// ``with(_:_:)`` or ``subscript(_:)`` instead. - public subscript(dynamicMember keyPath: KeyPath) -> Value { - get { dependencies[keyPath: keyPath] } + public subscript(dynamicMember keyPath: KeyPath<_ComposableDependencies, Value>) -> Value { + get { _dependencies[keyPath: keyPath] } } } diff --git a/Sources/ComposableEnvironment/Dependencies.swift b/Sources/ComposableEnvironment/Dependencies.swift index 141f1fe..78cb3af 100644 --- a/Sources/ComposableEnvironment/Dependencies.swift +++ b/Sources/ComposableEnvironment/Dependencies.swift @@ -69,3 +69,13 @@ public struct ComposableDependencies { } } } + +public final class _ComposableDependencies { + weak var context: _ComposableDependencies? + var values: [ObjectIdentifier: Any] = [:] + + public subscript(_ key: T.Type) -> T.Value where T: DependencyKey { + get { values[ObjectIdentifier(key)] as? T.Value ?? context?[key] ?? key.defaultValue } + set { values[ObjectIdentifier(key)] = newValue } + } +} diff --git a/Sources/ComposableEnvironment/Dependency.swift b/Sources/ComposableEnvironment/Dependency.swift index 3632344..28b6459 100644 --- a/Sources/ComposableEnvironment/Dependency.swift +++ b/Sources/ComposableEnvironment/Dependency.swift @@ -54,3 +54,60 @@ public struct Dependency { set { fatalError() } } } + +/// Use this property wrapper to declare depencies in a ``ComposableEnvironment`` subclass. +/// +/// You reference the dependency by its `KeyPath` originating from ``ComposableDependencies``, and +/// you declare its name in the local environment. The dependency should not be instantiated, as it +/// is either inherited from a ``ComposableEnvironment`` parent, or installed with +/// ``ComposableEnvironment/with(_:_:)``. +/// +/// For example, if the dependency is declared as: +/// ```swift +/// extension ComposableDependencies { +/// var uuidGenerator: () -> UUID { +/// get { self[UUIDGeneratorKey.self] } +/// set { self[UUIDGeneratorKey.self] = newValue } +/// } +/// }, +/// ``` +/// you can install it in `LocalEnvironment` like: +/// ```swift +/// class LocalEnvironment: ComposableEnvironment { +/// @Dependency(\.uuidGenerator) var uuid +/// } +/// ``` +/// This exposes a `var uuid: () -> UUID` read-only property in the `LocalEnvironment`. This +/// property can then be used as any vanilla dependency. +@propertyWrapper +public struct _Dependency { + /// Alternative to ``wrappedValue`` with access to the enclosing instance. + public static subscript( + _enclosingInstance instance: EnclosingSelf, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> Value { + get { + let wrapper = instance[keyPath: storageKeyPath] + let keyPath = wrapper.keyPath + let value = instance._dependencies[keyPath: keyPath] + return value + } + set { + fatalError("@Dependency are read-only in their ComposableEnvironment") + } + } + + var keyPath: KeyPath<_ComposableDependencies, Value> + + /// See ``Dependency`` discussion + public init(_ keyPath: KeyPath<_ComposableDependencies, Value>) { + self.keyPath = keyPath + } + + @available(*, unavailable, message: "@Dependency should be used in a ComposableEnvironment class.") + public var wrappedValue: Value { + get { fatalError() } + set { fatalError() } + } +} diff --git a/Sources/ComposableEnvironment/DerivedEnvironment.swift b/Sources/ComposableEnvironment/DerivedEnvironment.swift index 68da536..69f59e7 100644 --- a/Sources/ComposableEnvironment/DerivedEnvironment.swift +++ b/Sources/ComposableEnvironment/DerivedEnvironment.swift @@ -48,3 +48,54 @@ public final class DerivedEnvironment where Value: ComposableEnvironment set { fatalError() } } } + +/// Use this property wrapper to declare child ``ComposableEnvironment`` in a +/// ``ComposableEnvironment`` subclass. +/// +/// You only need to specify the subclass used and its name. You don't need to instantiate the +/// subclass. For example, if `ChildEnvironment` is a ``ComposableEnvironment`` subclass, you can +/// install a representant in `ParentEnvironment` as: +/// ```swift +/// class ParentEnvironment: ComposableEnvironment { +/// @DerivedEnvironment var child +/// }. +/// ``` +/// This exposes a `var child: ChildEnvironment` read-only property in the `ParentEnvironment`. +/// This child environment inherits the current dependencies of all its ancestor. They can be +/// exposed using the ``Dependency`` property wrapper. +@propertyWrapper +public final class _DerivedEnvironment where Value: ComposableEnvironment { + /// Alternative to ``wrappedValue`` with access to the enclosing instance. + public static subscript( + _enclosingInstance instance: EnclosingSelf, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> Value { + get { + instance[keyPath: storageKeyPath] + .environment + .connected(to: instance) + } + set { + fatalError("@DerivedEnvironments are read-only in their parent") + } + } + + var environment: Value + + /// See ``DerivedEnvironment`` discussion + public init(wrappedValue: Value) { + self.environment = wrappedValue + } + + /// See ``DerivedEnvironment`` discussion + public init() { + self.environment = Value() + } + + @available(*, unavailable, message: "@DerivedEnvironment should be used in a ComposableEnvironment class.") + public var wrappedValue: Value { + get { fatalError() } + set { fatalError() } + } +} diff --git a/Sources/ComposableEnvironment/Reducer+ComposableEnvironment.swift b/Sources/ComposableEnvironment/Reducer+ComposableEnvironment.swift index d92e5cc..0ecd4c3 100644 --- a/Sources/ComposableEnvironment/Reducer+ComposableEnvironment.swift +++ b/Sources/ComposableEnvironment/Reducer+ComposableEnvironment.swift @@ -25,7 +25,7 @@ public extension Reducer where Environment: ComposableEnvironment { return pullback( state: toLocalState, action: toLocalAction, - environment: local.updatingFromParentIfNeeded + environment: local.connected ) } @@ -58,7 +58,7 @@ public extension Reducer where Environment: ComposableEnvironment { return pullback( state: toLocalState, action: toLocalAction, - environment: local.updatingFromParentIfNeeded, + environment: local.connected, breakpointOnNil: breakpointOnNil ) } @@ -92,7 +92,7 @@ public extension Reducer where Environment: ComposableEnvironment { return forEach( state: toLocalState, action: toLocalAction, - environment: local.updatingFromParentIfNeeded, + environment: local.connected, breakpointOnNil: breakpointOnNil ) } @@ -125,7 +125,7 @@ public extension Reducer where Environment: ComposableEnvironment { return forEach( state: toLocalState, action: toLocalAction, - environment: local.updatingFromParentIfNeeded, + environment: local.connected, breakpointOnNil: breakpointOnNil ) } diff --git a/Tests/ComposableEnvironmentTests/ComposableEnvironmentTests.swift b/Tests/ComposableEnvironmentTests/ComposableEnvironmentTests.swift index 7a2016a..ff6b776 100644 --- a/Tests/ComposableEnvironmentTests/ComposableEnvironmentTests.swift +++ b/Tests/ComposableEnvironmentTests/ComposableEnvironmentTests.swift @@ -5,7 +5,7 @@ fileprivate struct IntKey: DependencyKey { static var defaultValue: Int { 1 } } -fileprivate extension ComposableDependencies { +fileprivate extension _ComposableDependencies { var int: Int { get { self[IntKey.self] } set { self[IntKey.self] = newValue } @@ -13,21 +13,21 @@ fileprivate extension ComposableDependencies { } final class ComposableEnvironmentTests: XCTestCase { - func testDependency() { + func test_Dependency() { class Env: ComposableEnvironment { - @Dependency(\.int) var int + @_Dependency(\.int) var int } let env = Env() XCTAssertEqual(env.int, 1) } - func testDependencyPropagation() { + func test_DependencyPropagation() { class Parent: ComposableEnvironment { - @Dependency(\.int) var int - @DerivedEnvironment var child + @_Dependency(\.int) var int + @_DerivedEnvironment var child } class Child: ComposableEnvironment { - @Dependency(\.int) var int + @_Dependency(\.int) var int } let parent = Parent() XCTAssertEqual(parent.child.int, 1) @@ -37,14 +37,14 @@ final class ComposableEnvironmentTests: XCTestCase { XCTAssertEqual(parentWith2.child.int, 2) } - func testDependencyOverride() { + func test_DependencyOverride() { class Parent: ComposableEnvironment { - @Dependency(\.int) var int - @DerivedEnvironment var child - @DerivedEnvironment var sibling = Child().with(\.int, 3) + @_Dependency(\.int) var int + @_DerivedEnvironment var child + @_DerivedEnvironment var sibling = Child().with(\.int, 3) } class Child: ComposableEnvironment { - @Dependency(\.int) var int + @_Dependency(\.int) var int } let parent = Parent().with(\.int, 2) @@ -55,12 +55,12 @@ final class ComposableEnvironmentTests: XCTestCase { func testDerivedWithProperties() { class Parent: ComposableEnvironment { - @Dependency(\.int) var int - @DerivedEnvironment var child - @DerivedEnvironment var sibling = Child(otherInt: 5).with(\.int, 3) + @_Dependency(\.int) var int + @_DerivedEnvironment var child + @_DerivedEnvironment var sibling = Child(otherInt: 5).with(\.int, 3) } final class Child: ComposableEnvironment { - @Dependency(\.int) var int + @_Dependency(\.int) var int var otherInt: Int = 4 required init() {} init(otherInt: Int) { @@ -79,24 +79,24 @@ final class ComposableEnvironmentTests: XCTestCase { func testLongChainsPropagation() { class Parent: ComposableEnvironment { - @Dependency(\.int) var int - @DerivedEnvironment var c1 + @_Dependency(\.int) var int + @_DerivedEnvironment var c1 } final class C1: ComposableEnvironment { - @DerivedEnvironment var c2 + @_DerivedEnvironment var c2 } final class C2: ComposableEnvironment { - @DerivedEnvironment var c3 + @_DerivedEnvironment var c3 } final class C3: ComposableEnvironment { - @DerivedEnvironment var c4 - @Dependency(\.int) var int + @_DerivedEnvironment var c4 + @_Dependency(\.int) var int } final class C4: ComposableEnvironment { - @DerivedEnvironment var c5 + @_DerivedEnvironment var c5 } final class C5: ComposableEnvironment { - @Dependency(\.int) var int + @_Dependency(\.int) var int } let parent = Parent().with(\.int, 4) XCTAssertEqual(parent.c1.c2.c3.c4.c5.int, 4) @@ -105,24 +105,24 @@ final class ComposableEnvironmentTests: XCTestCase { func testModifyingDependenciesOncePrimed() { class Parent: ComposableEnvironment { - @Dependency(\.int) var int - @DerivedEnvironment var c1 + @_Dependency(\.int) var int + @_DerivedEnvironment var c1 } final class C1: ComposableEnvironment { - @DerivedEnvironment var c2 + @_DerivedEnvironment var c2 } final class C2: ComposableEnvironment { - @DerivedEnvironment var c3 + @_DerivedEnvironment var c3 } final class C3: ComposableEnvironment { - @DerivedEnvironment var c4 - @Dependency(\.int) var int + @_DerivedEnvironment var c4 + @_Dependency(\.int) var int } final class C4: ComposableEnvironment { - @DerivedEnvironment var c5 + @_DerivedEnvironment var c5 } final class C5: ComposableEnvironment { - @Dependency(\.int) var int + @_Dependency(\.int) var int } let parent = Parent().with(\.int, 4) XCTAssertEqual(parent.c1.c2.c3.int, 4) diff --git a/Tests/ComposableEnvironmentTests/ReducerComposableEnvironmentTests.swift b/Tests/ComposableEnvironmentTests/ReducerComposableEnvironmentTests.swift index 2a8474e..004c222 100644 --- a/Tests/ComposableEnvironmentTests/ReducerComposableEnvironmentTests.swift +++ b/Tests/ComposableEnvironmentTests/ReducerComposableEnvironmentTests.swift @@ -6,7 +6,7 @@ fileprivate struct IntKey: DependencyKey { static var defaultValue: Int { 1 } } -fileprivate extension ComposableDependencies { +fileprivate extension _ComposableDependencies { var int: Int { get { self[IntKey.self] } set { self[IntKey.self] = newValue } @@ -175,10 +175,10 @@ final class ReducerAdditionsTests: XCTestCase { func testComposableAutoComposableComposableBridging() { class Third: ComposableEnvironment { - @Dependency(\.int) var integer + @_Dependency(\.int) var integer } class Second: ComposableEnvironment { - @DerivedEnvironment var third + @_DerivedEnvironment var third } class First: ComposableEnvironment {} @@ -214,7 +214,7 @@ final class ReducerAdditionsTests: XCTestCase { class Third: ComposableEnvironment { } class Second: ComposableEnvironment { } class First: ComposableEnvironment { - @DerivedEnvironment var second + @_DerivedEnvironment var second } enum Action {