Skip to content

Commit 0f34848

Browse files
Use DependencyValues.$_current.withValue(dependencies) to compute wrappedValue (#71)
* Add failing test * Compute dependency value using withValue * Fix test * update and add tests * remove #if DEBUG guard --------- Co-authored-by: Brandon Williams <[email protected]>
1 parent 0b18a15 commit 0f34848

File tree

3 files changed

+175
-56
lines changed

3 files changed

+175
-56
lines changed

Sources/Dependencies/Dependency.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,16 @@ public struct Dependency<Value>: @unchecked Sendable, _HasInitialValues {
9292
currentDependency.fileID = self.fileID
9393
currentDependency.line = self.line
9494
return DependencyValues.$currentDependency.withValue(currentDependency) {
95-
self.initialValues.merging(DependencyValues._current)[keyPath: self.keyPath]
95+
let dependencies = self.initialValues.merging(DependencyValues._current)
96+
return DependencyValues.$_current.withValue(dependencies) {
97+
DependencyValues._current[keyPath: self.keyPath]
98+
}
9699
}
97100
#else
98-
return self.initialValues.merging(DependencyValues._current)[keyPath: self.keyPath]
101+
let dependencies = self.initialValues.merging(DependencyValues._current)
102+
return DependencyValues.$_current.withValue(dependencies) {
103+
DependencyValues._current[keyPath: self.keyPath]
104+
}
99105
#endif
100106
}
101107
}

Sources/Dependencies/DependencyValues.swift

Lines changed: 50 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,7 @@ public struct DependencyValues: Sendable {
9595
/// provide access only to default values. Instead, you rely on the dependency values' instance
9696
/// that the library manages for you when you use the ``Dependency`` property wrapper.
9797
public init() {
98-
#if DEBUG
99-
_ = setUpTestObservers
100-
#endif
98+
_ = setUpTestObservers
10199
}
102100

103101
/// Accesses the dependency value associated with a custom key.
@@ -351,62 +349,60 @@ private final class CachedValues: @unchecked Sendable {
351349

352350
// NB: We cannot statically link/load XCTest on Apple platforms, so we dynamically load things
353351
// instead and we limit this to debug builds to avoid App Store binary rejection.
354-
#if DEBUG
355-
#if !canImport(ObjectiveC)
356-
import XCTest
357-
#endif
352+
#if !canImport(ObjectiveC)
353+
import XCTest
354+
#endif
358355

359-
private let setUpTestObservers: Void = {
360-
if _XCTIsTesting {
361-
#if canImport(ObjectiveC)
362-
DispatchQueue.mainSync {
363-
guard
364-
let XCTestObservation = objc_getProtocol("XCTestObservation"),
365-
let XCTestObservationCenter = NSClassFromString("XCTestObservationCenter"),
366-
let XCTestObservationCenter = XCTestObservationCenter as Any as? NSObjectProtocol,
367-
let XCTestObservationCenterShared =
368-
XCTestObservationCenter
369-
.perform(Selector(("sharedTestObservationCenter")))?
370-
.takeUnretainedValue()
371-
else { return }
372-
let testCaseWillStartBlock: @convention(block) (AnyObject) -> Void = { _ in
373-
DependencyValues._current.cachedValues.cached = [:]
374-
}
375-
let testCaseWillStartImp = imp_implementationWithBlock(testCaseWillStartBlock)
376-
class_addMethod(
377-
TestObserver.self, Selector(("testCaseWillStart:")), testCaseWillStartImp, nil)
378-
class_addProtocol(TestObserver.self, XCTestObservation)
379-
_ =
380-
XCTestObservationCenterShared
381-
.perform(Selector(("addTestObserver:")), with: TestObserver())
356+
private let setUpTestObservers: Void = {
357+
if _XCTIsTesting {
358+
#if canImport(ObjectiveC)
359+
DispatchQueue.mainSync {
360+
guard
361+
let XCTestObservation = objc_getProtocol("XCTestObservation"),
362+
let XCTestObservationCenter = NSClassFromString("XCTestObservationCenter"),
363+
let XCTestObservationCenter = XCTestObservationCenter as Any as? NSObjectProtocol,
364+
let XCTestObservationCenterShared =
365+
XCTestObservationCenter
366+
.perform(Selector(("sharedTestObservationCenter")))?
367+
.takeUnretainedValue()
368+
else { return }
369+
let testCaseWillStartBlock: @convention(block) (AnyObject) -> Void = { _ in
370+
DependencyValues._current.cachedValues.cached = [:]
382371
}
383-
#else
384-
XCTestObservationCenter.shared.addTestObserver(TestObserver())
385-
#endif
386-
}
387-
}()
388-
389-
#if canImport(ObjectiveC)
390-
private final class TestObserver: NSObject {}
391-
#else
392-
private final class TestObserver: NSObject, XCTestObservation {
393-
func testCaseWillStart(_ testCase: XCTestCase) {
394-
DependencyValues._current.cachedValues.cached = [:]
372+
let testCaseWillStartImp = imp_implementationWithBlock(testCaseWillStartBlock)
373+
class_addMethod(
374+
TestObserver.self, Selector(("testCaseWillStart:")), testCaseWillStartImp, nil)
375+
class_addProtocol(TestObserver.self, XCTestObservation)
376+
_ =
377+
XCTestObservationCenterShared
378+
.perform(Selector(("addTestObserver:")), with: TestObserver())
395379
}
380+
#else
381+
XCTestObservationCenter.shared.addTestObserver(TestObserver())
382+
#endif
383+
}
384+
}()
385+
386+
#if canImport(ObjectiveC)
387+
private final class TestObserver: NSObject {}
388+
#else
389+
private final class TestObserver: NSObject, XCTestObservation {
390+
func testCaseWillStart(_ testCase: XCTestCase) {
391+
DependencyValues._current.cachedValues.cached = [:]
396392
}
397-
#endif
393+
}
394+
#endif
398395

399-
extension DispatchQueue {
400-
private static let key = DispatchSpecificKey<UInt8>()
401-
private static let value: UInt8 = 0
396+
extension DispatchQueue {
397+
private static let key = DispatchSpecificKey<UInt8>()
398+
private static let value: UInt8 = 0
402399

403-
fileprivate static func mainSync<R>(execute block: @Sendable () -> R) -> R {
404-
Self.main.setSpecific(key: Self.key, value: Self.value)
405-
if getSpecific(key: Self.key) == Self.value {
406-
return block()
407-
} else {
408-
return Self.main.sync(execute: block)
409-
}
400+
fileprivate static func mainSync<R>(execute block: @Sendable () -> R) -> R {
401+
Self.main.setSpecific(key: Self.key, value: Self.value)
402+
if getSpecific(key: Self.key) == Self.value {
403+
return block()
404+
} else {
405+
return Self.main.sync(execute: block)
410406
}
411407
}
412-
#endif
408+
}

Tests/DependenciesTests/ResolutionTests.swift

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,102 @@ final class ResolutionTests: XCTestCase {
4747
}
4848
}
4949

50+
func testDependencyDependingOnDependency_Nested() {
51+
struct Model {
52+
@Dependency(\.nestedParent) var nestedParent: NestedParentDependency
53+
@Dependency(\.nestedChild) var nestedChild: NestedChildDependency
54+
}
55+
56+
let model = withDependencies {
57+
$0.nestedChild.value = { 1 }
58+
} operation: {
59+
Model()
60+
}
61+
62+
XCTAssertEqual(model.nestedParent.value(), 1)
63+
XCTAssertEqual(model.nestedChild.value(), 1)
64+
65+
withDependencies {
66+
$0.nestedChild.value = { 42 }
67+
} operation: {
68+
XCTAssertEqual(model.nestedParent.value(), 42)
69+
XCTAssertEqual(model.nestedChild.value(), 42)
70+
}
71+
}
72+
73+
func testFirstAccessBehavior() {
74+
@Dependency(\.nestedParent) var nestedParent: NestedParentDependency
75+
struct Model {
76+
@Dependency(\.nestedParent) var nestedParent: NestedParentDependency
77+
@Dependency(\.nestedChild) var nestedChild: NestedChildDependency
78+
}
79+
80+
let model = withDependencies {
81+
$0.nestedChild.value = { 1 }
82+
} operation: {
83+
Model()
84+
}
85+
86+
XCTAssertEqual(nestedParent.value(), 1729)
87+
XCTAssertEqual(model.nestedParent.value(), 1729)
88+
XCTAssertEqual(model.nestedChild.value(), 1)
89+
90+
withDependencies {
91+
$0.nestedChild.value = { 42 }
92+
} operation: {
93+
XCTAssertEqual(model.nestedParent.value(), 42)
94+
XCTAssertEqual(model.nestedChild.value(), 42)
95+
}
96+
}
97+
98+
func testParentChildScoping() {
99+
withDependencies {
100+
$0.context = .live
101+
} operation: {
102+
@Dependency(\.date) var date
103+
let _ = date.now
104+
105+
class ParentModel {
106+
@Dependency(\.date) var date
107+
var child1: Child1Model?
108+
var child2: Child2Model?
109+
func goToChild1() {
110+
self.child1 = withDependencies(from: self) { Child1Model() }
111+
}
112+
func goToChild2() {
113+
self.child2 = withDependencies(from: self) { Child2Model() }
114+
}
115+
}
116+
class Child1Model {
117+
@Dependency(\.date) var date
118+
}
119+
class Child2Model {
120+
@Dependency(\.date) var date
121+
}
122+
123+
let model = withDependencies {
124+
$0.date = .constant(Date(timeIntervalSince1970: 1))
125+
} operation: {
126+
ParentModel()
127+
}
128+
129+
withDependencies {
130+
$0.date = .constant(Date(timeIntervalSince1970: 2))
131+
} operation: {
132+
model.goToChild1()
133+
}
134+
withDependencies {
135+
$0.date = .constant(Date(timeIntervalSince1970: 3))
136+
} operation: {
137+
model.goToChild2()
138+
}
139+
140+
XCTAssertEqual(model.date.now.timeIntervalSince1970, 1)
141+
XCTAssertEqual(model.child1?.date.now.timeIntervalSince1970, 2)
142+
XCTAssertEqual(model.child2?.date.now.timeIntervalSince1970, 3)
143+
}
144+
}
145+
50146
func testDependencyDiamond() {
51147
@Dependency(\.diamondA) var diamondA: DiamondDependencyA
52148
@Dependency(\.diamondB1) var diamondB1: DiamondDependencyB1
@@ -111,6 +207,19 @@ private struct LazyChildDependency: TestDependencyKey {
111207

112208
static let testValue = Self { 1729 }
113209
}
210+
private struct NestedParentDependency: TestDependencyKey {
211+
var value: () -> Int
212+
static var testValue: NestedParentDependency {
213+
@Dependency(\.nestedChild) var child
214+
return Self {
215+
return child.value()
216+
}
217+
}
218+
}
219+
private struct NestedChildDependency: TestDependencyKey {
220+
var value: () -> Int
221+
static let testValue = Self { 1729 }
222+
}
114223
private struct DiamondDependencyA: TestDependencyKey {
115224
var value: @Sendable () -> Int
116225
static let testValue = Self {
@@ -169,6 +278,14 @@ extension DependencyValues {
169278
get { self[LazyChildDependency.self] }
170279
set { self[LazyChildDependency.self] = newValue }
171280
}
281+
fileprivate var nestedParent: NestedParentDependency {
282+
get { self[NestedParentDependency.self] }
283+
set { self[NestedParentDependency.self] = newValue }
284+
}
285+
fileprivate var nestedChild: NestedChildDependency {
286+
get { self[NestedChildDependency.self] }
287+
set { self[NestedChildDependency.self] = newValue }
288+
}
172289
fileprivate var diamondA: DiamondDependencyA {
173290
get { self[DiamondDependencyA.self] }
174291
set { self[DiamondDependencyA.self] = newValue }

0 commit comments

Comments
 (0)