Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
runs-on: macos-12
strategy:
matrix:
xcode: ['14.2']
xcode: ['13.4.1', '14.2']
config: ['debug', 'release']
steps:
- uses: actions/checkout@v3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,28 @@ extension DependencyValues {
///
/// See ``DependencyValues/withRandomNumberGenerator`` for more information.
public final class WithRandomNumberGenerator: @unchecked Sendable {
private var generator: RandomNumberGenerator
private var generator: _AnyRandomNumberGenerator
private let lock = NSLock()

public init<T: RandomNumberGenerator & Sendable>(_ generator: T) {
self.generator = generator
self.generator = _AnyRandomNumberGenerator(generator)
}

public func callAsFunction<R>(_ work: (inout RandomNumberGenerator) -> R) -> R {
public func callAsFunction<R>(_ work: (inout _AnyRandomNumberGenerator) -> R) -> R {
self.lock.lock()
defer { self.lock.unlock() }
return work(&self.generator)
}

public class _AnyRandomNumberGenerator: RandomNumberGenerator {
private(set) public var base: RandomNumberGenerator

public init(_ base: RandomNumberGenerator) {
self.base = base
}

public func next() -> UInt64 {
self.base.next()
}
}
}
273 changes: 175 additions & 98 deletions Sources/Dependencies/WithDependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,45 +39,67 @@ public func withDependencies<R>(
}
}

/// Updates the current dependencies for the duration of an asynchronous operation.
///
/// Any mutations made to ``DependencyValues`` inside `updateValuesForOperation` will be visible to
/// everything executed in the operation. For example, if you wanted to force the
/// ``DependencyValues/date`` dependency to be a particular date, you can do:
///
/// ```swift
/// await withDependencies {
/// $0.date.now = Date(timeIntervalSince1970: 1234567890)
/// } operation: {
/// // References to date in here are pinned to 1234567890.
/// }
/// ```
///
/// - Parameters:
/// - updateValuesForOperation: A closure for updating the current dependency values for the
/// duration of the operation.
/// - operation: An operation to perform wherein dependencies have been overridden.
/// - Returns: The result returned from `operation`.
@_unsafeInheritExecutor
@discardableResult
public func withDependencies<R>(
_ updateValuesForOperation: (inout DependencyValues) async throws -> Void,
operation: () async throws -> R
) async rethrows -> R {
try await DependencyValues.$isSetting.withValue(true) {
var dependencies = DependencyValues._current
try await updateValuesForOperation(&dependencies)
return try await DependencyValues.$_current.withValue(dependencies) {
try await DependencyValues.$isSetting.withValue(false) {
let result = try await operation()
if R.self is AnyClass {
dependencyObjects.store(result as AnyObject)
#if swift(>=5.7)
/// Updates the current dependencies for the duration of an asynchronous operation.
///
/// Any mutations made to ``DependencyValues`` inside `updateValuesForOperation` will be visible
/// to everything executed in the operation. For example, if you wanted to force the
/// ``DependencyValues/date`` dependency to be a particular date, you can do:
///
/// ```swift
/// await withDependencies {
/// $0.date.now = Date(timeIntervalSince1970: 1234567890)
/// } operation: {
/// // References to date in here are pinned to 1234567890.
/// }
/// ```
///
/// - Parameters:
/// - updateValuesForOperation: A closure for updating the current dependency values for the
/// duration of the operation.
/// - operation: An operation to perform wherein dependencies have been overridden.
/// - Returns: The result returned from `operation`.
@_unsafeInheritExecutor
@discardableResult
public func withDependencies<R>(
_ updateValuesForOperation: (inout DependencyValues) async throws -> Void,
operation: () async throws -> R
) async rethrows -> R {
try await DependencyValues.$isSetting.withValue(true) {
var dependencies = DependencyValues._current
try await updateValuesForOperation(&dependencies)
return try await DependencyValues.$_current.withValue(dependencies) {
try await DependencyValues.$isSetting.withValue(false) {
let result = try await operation()
if R.self is AnyClass {
dependencyObjects.store(result as AnyObject)
}
return result
}
return result
}
}
}
}
#else
@discardableResult
public func withDependencies<R>(
_ updateValuesForOperation: (inout DependencyValues) async throws -> Void,
operation: () async throws -> R
) async rethrows -> R {
try await DependencyValues.$isSetting.withValue(true) {
var dependencies = DependencyValues._current
try await updateValuesForOperation(&dependencies)
return try await DependencyValues.$_current.withValue(dependencies) {
try await DependencyValues.$isSetting.withValue(false) {
let result = try await operation()
if R.self is AnyClass {
dependencyObjects.store(result as AnyObject)
}
return result
}
}
}
}
#endif

/// Updates the current dependencies for the duration of a synchronous operation by taking the
/// dependencies tied to a given object.
Expand Down Expand Up @@ -146,74 +168,129 @@ public func withDependencies<Model: AnyObject, R>(
)
}

/// Updates the current dependencies for the duration of an asynchronous operation by taking the
/// dependencies tied to a given object.
///
/// - Parameters:
/// - model: An object with dependencies. The given model should have at least one `@Dependency`
/// property, or should have been initialized and returned from a `withDependencies` operation.
/// - updateValuesForOperation: A closure for updating the current dependency values for the
/// duration of the operation.
/// - operation: The operation to run with the updated dependencies.
/// - Returns: The result returned from `operation`.
@_unsafeInheritExecutor
@discardableResult
public func withDependencies<Model: AnyObject, R>(
from model: Model,
_ updateValuesForOperation: (inout DependencyValues) async throws -> Void,
operation: () async throws -> R,
file: StaticString? = nil,
line: UInt? = nil
) async rethrows -> R {
guard let values = dependencyObjects.values(from: model)
else {
runtimeWarn(
"""
You are trying to propagate dependencies to a child model from a model with no dependencies. \
To fix this, the given '\(Model.self)' must be returned from another 'withDependencies' \
closure, or the class must hold at least one '@Dependency' property.
""",
#if swift(>=5.7)
/// Updates the current dependencies for the duration of an asynchronous operation by taking the
/// dependencies tied to a given object.
///
/// - Parameters:
/// - model: An object with dependencies. The given model should have at least one `@Dependency`
/// property, or should have been initialized and returned from a `withDependencies`
/// operation.
/// - updateValuesForOperation: A closure for updating the current dependency values for the
/// duration of the operation.
/// - operation: The operation to run with the updated dependencies.
/// - Returns: The result returned from `operation`.
@_unsafeInheritExecutor
@discardableResult
public func withDependencies<Model: AnyObject, R>(
from model: Model,
_ updateValuesForOperation: (inout DependencyValues) async throws -> Void,
operation: () async throws -> R,
file: StaticString? = nil,
line: UInt? = nil
) async rethrows -> R {
guard let values = dependencyObjects.values(from: model)
else {
runtimeWarn(
"""
You are trying to propagate dependencies to a child model from a model with no \
dependencies. To fix this, the given '\(Model.self)' must be returned from another \
'withDependencies' closure, or the class must hold at least one '@Dependency' property.
""",
file: file,
line: line
)
return try await operation()
}
return try await withDependencies {
$0 = values.merging(DependencyValues._current)
try await updateValuesForOperation(&$0)
} operation: {
let result = try await operation()
if R.self is AnyClass {
dependencyObjects.store(result as AnyObject)
}
return result
}
}
#else
@discardableResult
public func withDependencies<Model: AnyObject, R>(
from model: Model,
_ updateValuesForOperation: (inout DependencyValues) async throws -> Void,
operation: () async throws -> R,
file: StaticString? = nil,
line: UInt? = nil
) async rethrows -> R {
guard let values = dependencyObjects.values(from: model)
else {
runtimeWarn(
"""
You are trying to propagate dependencies to a child model from a model with no \
dependencies. To fix this, the given '\(Model.self)' must be returned from another \
'withDependencies' closure, or the class must hold at least one '@Dependency' property.
""",
file: file,
line: line
)
return try await operation()
}
return try await withDependencies {
$0 = values.merging(DependencyValues._current)
try await updateValuesForOperation(&$0)
} operation: {
let result = try await operation()
if R.self is AnyClass {
dependencyObjects.store(result as AnyObject)
}
return result
}
}
#endif

#if swift(>=5.7)
/// Updates the current dependencies for the duration of an asynchronous operation by taking the
/// dependencies tied to a given object.
///
/// - Parameters:
/// - model: An object with dependencies. The given model should have at least one `@Dependency`
/// property, or should have been initialized and returned from a `withDependencies`
/// operation.
/// - operation: The operation to run with the updated dependencies.
/// - Returns: The result returned from `operation`.
@_unsafeInheritExecutor
@discardableResult
public func withDependencies<Model: AnyObject, R>(
from model: Model,
operation: () async throws -> R,
file: StaticString? = nil,
line: UInt? = nil
) async rethrows -> R {
try await withDependencies(
from: model,
{ _ in },
operation: operation,
file: file,
line: line
)
return try await operation()
}
return try await withDependencies {
$0 = values.merging(DependencyValues._current)
try await updateValuesForOperation(&$0)
} operation: {
let result = try await operation()
if R.self is AnyClass {
dependencyObjects.store(result as AnyObject)
}
return result
#else
@discardableResult
public func withDependencies<Model: AnyObject, R>(
from model: Model,
operation: () async throws -> R,
file: StaticString? = nil,
line: UInt? = nil
) async rethrows -> R {
try await withDependencies(
from: model,
{ _ in },
operation: operation,
file: file,
line: line
)
}
}

/// Updates the current dependencies for the duration of an asynchronous operation by taking the
/// dependencies tied to a given object.
///
/// - Parameters:
/// - model: An object with dependencies. The given model should have at least one `@Dependency`
/// property, or should have been initialized and returned from a `withDependencies` operation.
/// - operation: The operation to run with the updated dependencies.
/// - Returns: The result returned from `operation`.
@_unsafeInheritExecutor
@discardableResult
public func withDependencies<Model: AnyObject, R>(
from model: Model,
operation: () async throws -> R,
file: StaticString? = nil,
line: UInt? = nil
) async rethrows -> R {
try await withDependencies(
from: model,
{ _ in },
operation: operation,
file: file,
line: line
)
}
#endif

/// Propagates the current dependencies to an escaping context.
///
Expand Down
2 changes: 1 addition & 1 deletion Tests/DependenciesTests/DependencyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ final class DependencyTests: XCTestCase {
await withDependencies {
await Task.yield()
$0.string = "howdy"
} operation: {
} operation: { () -> Model in
await Task.yield()
return Model()
}
Expand Down
8 changes: 4 additions & 4 deletions Tests/DependenciesTests/DependencyValuesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ final class DependencyValuesTests: XCTestCase {

withDependencies {
$0.context = .preview
} operation: {
} operation: { () -> Void in
XCTAssertEqual(reuseClient.count(), 0)
reuseClient.setCount(1729)
XCTAssertEqual(reuseClient.count(), 1729)
Expand Down Expand Up @@ -451,7 +451,7 @@ final class DependencyValuesTests: XCTestCase {
func testTaskPropagation() async throws {
let task = withDependencies {
$0.date.now = Date(timeIntervalSinceReferenceDate: 1_234_567_890)
} operation: {
} operation: { () -> Task<Void, Never> in
@Dependency(\.date.now) var now: Date
XCTAssertEqual(now.timeIntervalSinceReferenceDate, 1_234_567_890)
return Task {
Expand Down Expand Up @@ -483,7 +483,7 @@ final class DependencyValuesTests: XCTestCase {
func testAsyncStreamUnfoldingWithoutEscapedDependencies() async {
let stream = withDependencies {
$0.fullDependency.value = 42
} operation: {
} operation: { () -> AsyncStream<Int> in
var isDone = false
return AsyncStream(unfolding: {
defer { isDone = true }
Expand All @@ -499,7 +499,7 @@ final class DependencyValuesTests: XCTestCase {
func testAsyncStreamUnfoldingWithEscapedDependencies() async {
let stream = withDependencies {
$0.fullDependency.value = 42
} operation: {
} operation: { () -> AsyncStream<Int> in
var isDone = false
return withEscapedDependencies { continuation in
AsyncStream(unfolding: {
Expand Down
Loading