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

wip: Expose ComposableEnvironment.updatingFromParentIfNeeded to public API #4

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
/*.xcodeproj
xcuserdata/
Package.resolved
.swiftpm
7 changes: 0 additions & 7 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

This file was deleted.

This file was deleted.

26 changes: 17 additions & 9 deletions Sources/ComposableEnvironment/ComposableEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -46,7 +54,7 @@ open class ComposableEnvironment {
var upToDateDerivedEnvironments: NSHashTable<ComposableEnvironment> = .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.
Expand Down Expand Up @@ -77,23 +85,23 @@ open class ComposableEnvironment {
/// .with(\.mainQueue, .main)
/// ```
@discardableResult
public func with<V>(_ keyPath: WritableKeyPath<ComposableDependencies, V>, _ value: V) -> Self {
dependencies[keyPath: keyPath] = value
public func with<V>(_ 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<Value>(keyPath: WritableKeyPath<ComposableDependencies, Value>) -> Value {
get { dependencies[keyPath: keyPath] }
set { dependencies[keyPath: keyPath] = newValue }
public subscript<Value>(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<Value>(dynamicMember keyPath: KeyPath<ComposableDependencies, Value>) -> Value {
get { dependencies[keyPath: keyPath] }
public subscript<Value>(dynamicMember keyPath: KeyPath<_ComposableDependencies, Value>) -> Value {
get { _dependencies[keyPath: keyPath] }
}
}
10 changes: 10 additions & 0 deletions Sources/ComposableEnvironment/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,13 @@ public struct ComposableDependencies {
}
}
}

public final class _ComposableDependencies {
weak var context: _ComposableDependencies?
var values: [ObjectIdentifier: Any] = [:]

public subscript<T>(_ key: T.Type) -> T.Value where T: DependencyKey {
get { values[ObjectIdentifier(key)] as? T.Value ?? context?[key] ?? key.defaultValue }
set { values[ObjectIdentifier(key)] = newValue }
}
}
57 changes: 57 additions & 0 deletions Sources/ComposableEnvironment/Dependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,60 @@ public struct Dependency<Value> {
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<Value> {
/// Alternative to ``wrappedValue`` with access to the enclosing instance.
public static subscript<EnclosingSelf: ComposableEnvironment>(
_enclosingInstance instance: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
) -> 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() }
}
}
51 changes: 51 additions & 0 deletions Sources/ComposableEnvironment/DerivedEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,54 @@ public final class DerivedEnvironment<Value> 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<ChildEnvironment> 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<Value> where Value: ComposableEnvironment {
/// Alternative to ``wrappedValue`` with access to the enclosing instance.
public static subscript<EnclosingSelf: ComposableEnvironment>(
_enclosingInstance instance: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, _DerivedEnvironment>
) -> 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() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public extension Reducer where Environment: ComposableEnvironment {
return pullback(
state: toLocalState,
action: toLocalAction,
environment: local.updatingFromParentIfNeeded
environment: local.connected
)
}

Expand Down Expand Up @@ -58,7 +58,7 @@ public extension Reducer where Environment: ComposableEnvironment {
return pullback(
state: toLocalState,
action: toLocalAction,
environment: local.updatingFromParentIfNeeded,
environment: local.connected,
breakpointOnNil: breakpointOnNil
)
}
Expand Down Expand Up @@ -92,7 +92,7 @@ public extension Reducer where Environment: ComposableEnvironment {
return forEach(
state: toLocalState,
action: toLocalAction,
environment: local.updatingFromParentIfNeeded,
environment: local.connected,
breakpointOnNil: breakpointOnNil
)
}
Expand Down Expand Up @@ -125,7 +125,7 @@ public extension Reducer where Environment: ComposableEnvironment {
return forEach(
state: toLocalState,
action: toLocalAction,
environment: local.updatingFromParentIfNeeded,
environment: local.connected,
breakpointOnNil: breakpointOnNil
)
}
Expand Down
Loading