Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
stephencelis committed Sep 11, 2024
1 parent fc70f0a commit f12cc79
Show file tree
Hide file tree
Showing 20 changed files with 277 additions and 203 deletions.
10 changes: 6 additions & 4 deletions [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ let package = Package(
name: "DependenciesMacros",
targets: ["DependenciesMacros"]
),
.library(
name: "DependenciesTestSupport",
targets: ["DependenciesTestSupport"]
),
// NB: A Swift bug prevents the test trait from being useful at the moment.
// https://github.com/swiftlang/swift/issues/76409
// .library(
// name: "DependenciesTestSupport",
// targets: ["DependenciesTestSupport"]
// ),
],
dependencies: [
.package(url: "https://github.com/pointfreeco/combine-schedulers", from: "1.0.2"),
Expand Down
39 changes: 24 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,17 @@ is a good chance you can immediately make use of one. If you are using `Date()`,
this library.

```swift
final class FeatureModel: ObservableObject {
@Observable
final class FeatureModel {
var items: [Item] = []

@ObservationIgnored
@Dependency(\.continuousClock) var clock // Controllable way to sleep a task
@ObservationIgnored
@Dependency(\.date.now) var now // Controllable way to ask for current date
@ObservationIgnored
@Dependency(\.mainQueue) var mainQueue // Controllable scheduling on main queue
@ObservationIgnored
@Dependency(\.uuid) var uuid // Controllable UUID creation

// ...
Expand All @@ -96,16 +103,17 @@ Once your dependencies are declared, rather than reaching out to the `Date()`, `
directly, you can use the dependency that is defined on your feature's model:

```swift
final class FeatureModel: ObservableObject {
@Observable
final class FeatureModel {
// ...

func addButtonTapped() async throws {
try await self.clock.sleep(for: .seconds(1)) // 👈 Don't use 'Task.sleep'
self.items.append(
try await clock.sleep(for: .seconds(1)) // 👈 Don't use 'Task.sleep'
items.append(
Item(
id: self.uuid(), // 👈 Don't use 'UUID()'
id: uuid(), // 👈 Don't use 'UUID()'
name: "",
createdAt: self.now // 👈 Don't use 'Date()'
createdAt: now // 👈 Don't use 'Date()'
)
)
}
Expand All @@ -120,16 +128,17 @@ inside the `addButtonTapped` method, you can use the `.dependency` test trait
to override any dependencies for the scope of one single test. It's as easy as 1-2-3:

```swift
@Test(
// 1️⃣ Override any dependencies that your feature uses.
.dependency(\.clock, .immediate),
.dependency(\.date.now, Date(timeIntervalSinceReferenceDate: 1234567890)),
.dependency(\.uuid, .incrementing)
)
@Test
func add() async throws {
// 2️⃣ Construct the feature's model
let model = FeatureModel()

withDependencies {
// 1️⃣ Override any dependencies that your feature uses.
$0.clock = .immediate
$0.date.now = Date(timeIntervalSinceReferenceDate: 1234567890)
$0.uuid = .incrementing
} operation: {
// 2️⃣ Construct the feature's model
FeatureModel()
}
// 3️⃣ The model now executes in a controlled environment of dependencies,
// and so we can make assertions against its behavior.
try await model.addButtonTapped()
Expand Down
17 changes: 13 additions & 4 deletions Sources/Dependencies/Dependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
/// an observable object:
///
/// ```swift
/// final class FeatureModel: ObservableObject {
/// @Observable
/// final class FeatureModel {
/// @ObservationIgnored
/// @Dependency(\.apiClient) var apiClient
/// @ObservationIgnored
/// @Dependency(\.continuousClock) var clock
/// @ObservationIgnored
/// @Dependency(\.uuid) var uuid
///
/// // ...
Expand All @@ -18,7 +22,8 @@
/// Or, if you are using [the Composable Architecture][tca]:
///
/// ```swift
/// struct Feature: ReducerProtocol {
/// @Reducer
/// struct Feature {
/// @Dependency(\.apiClient) var apiClient
/// @Dependency(\.continuousClock) var clock
/// @Dependency(\.uuid) var uuid
Expand Down Expand Up @@ -108,7 +113,9 @@
/// reflect:
///
/// ```swift
/// final class FeatureModel: ObservableObject {
/// @Observable
/// final class FeatureModel {
/// @ObservationIgnored
/// @Dependency(\.date) var date
///
/// // ...
Expand Down Expand Up @@ -158,7 +165,9 @@ extension Dependency {
/// One can access the dependency using this property wrapper:
///
/// ```swift
/// final class FeatureModel: ObservableObject {
/// @Observable
/// final class FeatureModel {
/// @ObservationIgnored
/// @Dependency(Settings.self) var settings
///
/// // ...
Expand Down
10 changes: 6 additions & 4 deletions Sources/Dependencies/DependencyKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,13 @@ extension DependencyKey {
/// accessed:
///
/// ```swift
/// @Test(
/// .dependency(\.myDependency, .mock) // Override dependency
/// )
/// @Test
/// func featureThatUsesMyDependency() {
/// // Test feature with dependency overridden
/// withDependencies {
/// $0.myDependency = .mock // Override dependency
/// } operation: {
/// // Test feature with dependency overridden
/// }
/// }
/// ```
///
Expand Down
4 changes: 3 additions & 1 deletion Sources/Dependencies/DependencyValues/Date.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ extension DependencyValues {
/// wrapper to the generator's ``DateGenerator/now`` property:
///
/// ```swift
/// final class FeatureModel: ObservableObject {
/// @Observable
/// final class FeatureModel {
/// @ObservationIgnored
/// @Dependency(\.date.now) var now
/// // ...
/// }
Expand Down
4 changes: 3 additions & 1 deletion Sources/Dependencies/DependencyValues/Locale.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ extension DependencyValues {
/// wrapper to the property:
///
/// ```swift
/// final class FeatureModel: ObservableObject {
/// @Observable
/// final class FeatureModel {
/// @ObservationIgnored
/// @Dependency(\.locale) var locale
/// // ...
/// }
Expand Down
23 changes: 13 additions & 10 deletions Sources/Dependencies/DependencyValues/MainQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@
/// counts the number of seconds it's onscreen:
///
/// ```
/// final class TimerModel: ObservableObject {
/// @Published var elapsed = 0
/// @Observable
/// final class TimerModel {
/// var elapsed = 0
///
/// @ObservationIgnored
/// @Dependency(\.mainQueue) var mainQueue
///
/// @MainActor
/// func onAppear() async {
/// for await _ in self.mainQueue.timer(interval: .seconds(1)) {
/// self.elapsed += 1
/// for await _ in mainQueue.timer(interval: .seconds(1)) {
/// elapsed += 1
/// }
/// }
/// }
Expand All @@ -30,13 +32,14 @@
/// And you could test this model by overriding its main queue with a test scheduler:
///
/// ```
/// let mainQueue = DispatchQueue.test
///
/// @Test(
/// .dependency(\.mainQueue, mainQueue.eraseToAnyScheduler())
/// )
/// @Test
/// func feature() {
/// let model = TimerModel()
/// let mainQueue = DispatchQueue.test
/// let model = withDependencies {
/// $0.mainQueue = mainQueue.eraseToAnyScheduler()
/// } operation: {
/// TimerModel()
/// }
///
/// Task { await model.onAppear() }
///
Expand Down
27 changes: 15 additions & 12 deletions Sources/Dependencies/DependencyValues/MainRunLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,34 @@
/// For example, you could introduce controllable timing to an observable object model that
/// counts the number of seconds it's onscreen:
///
/// ```
/// struct TimerModel: ObservableObject {
/// @Published var elapsed = 0
/// ```swift
/// @Observable
/// struct TimerModel {
/// var elapsed = 0
///
/// @ObservationIgnored
/// @Dependency(\.mainRunLoop) var mainRunLoop
///
/// @MainActor
/// func onAppear() async {
/// for await _ in self.mainRunLoop.timer(interval: .seconds(1)) {
/// self.elapsed += 1
/// for await _ in mainRunLoop.timer(interval: .seconds(1)) {
/// elapsed += 1
/// }
/// }
/// }
/// ```
///
/// And you could test this model by overriding its main run loop with a test scheduler:
///
/// ```
/// let mainRunLoop = RunLoop.test
///
/// @Test(
/// .dependency(\.mainRunLoop, mainRunLoop.eraseToAnyScheduler())
/// )
/// ```swift
/// @Test
/// func feature() {
/// let model = TimerModel()
/// let mainRunLoop = RunLoop.test
/// let model = withDependencies {
/// $0.mainRunLoop = mainRunLoop.eraseToAnyScheduler()
/// } operation: {
/// TimerModel()
/// }
///
/// Task { await model.onAppear() }
///
Expand Down
19 changes: 12 additions & 7 deletions Sources/Dependencies/DependencyValues/UUID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ extension DependencyValues {
/// that creates to-dos with unique identifiers:
///
/// ```swift
/// final class TodosModel: ObservableObject {
/// @Published var todos: [Todo] = []
/// @Observable
/// final class TodosModel {
/// var todos: [Todo] = []
///
/// @ObservationIgnored
/// @Dependency(\.uuid) var uuid
///
/// func addButtonTapped() {
/// self.todos.append(Todo(id: self.uuid()))
/// todos.append(Todo(id: uuid()))
/// }
/// }
/// ```
Expand All @@ -39,11 +42,13 @@ extension DependencyValues {
/// ``UUIDGenerator/incrementing`` generator as a dependency:
///
/// ```swift
/// @Test(
/// .dependency(\.uuid, .incrementing)
/// )
/// @Test
/// func feature() {
/// let model = TodosModel()
/// let model = withDependencies {
/// $0.uuid = .incrementing
/// } operation: {
/// TodosModel()
/// }
///
/// model.addButtonTapped()
/// #expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ extension DependencyValues {
/// handles rolling a couple dice:
///
/// ```swift
/// final class GameModel: ObservableObject {
/// @Published var dice = (1, 1)
/// @Observable
/// final class GameModel {
/// var dice = (1, 1)
///
/// @ObservationIgnored
/// @Dependency(\.withRandomNumberGenerator) var withRandomNumberGenerator
///
/// func rollDice() {
/// self.dice = self.withRandomNumberGenerator { generator in
/// dice = withRandomNumberGenerator { generator in
/// (
/// Int.random(in: 1...6, using: &generator),
/// Int.random(in: 1...6, using: &generator)
/// .random(in: 1...6, using: &generator),
/// .random(in: 1...6, using: &generator)
/// )
/// }
/// }
Expand All @@ -40,11 +42,13 @@ extension DependencyValues {
/// a game's model by supplying a seeded random number generator as a dependency:
///
/// ```swift
/// @Test(
/// .dependency(\.withRandomNumberGenerator, WithRandomNumberGenerator(LCRNG(seed: 0)))
/// )
/// @Test
/// func roll() {
/// let model = GameModel()
/// let model = withDependencies {
/// $0.withRandomNumberGenerator = WithRandomNumberGenerator(LCRNG(seed: 0))
/// } operation: {
/// GameModel()
/// }
///
/// model.rollDice()
/// XCTAssert(model.dice == (1, 3))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ needs the `play` endpoint, and doesn't need to loop, set volume or stop audio, t
a dependency on just that one function:

```swift
final class FeatureModel: ObservableObject {
@Observable
final class FeatureModel {
@ObservationIgnored
@Dependency(\.audioPlayer.play) var play
// ...
}
Expand Down
10 changes: 7 additions & 3 deletions Sources/Dependencies/Documentation.docc/Articles/Lifetimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,16 @@ with a few tools to prolong the change in a well-defined manner.
For example, suppose you have a feature that needs access to an API client for fetching a user:

```swift
class FeatureModel: ObservableObject {
@Observable
class FeatureModel {
var user: User?

@ObservationIgnored
@Dependency(\.apiClient) var apiClient

func onAppear() async {
do {
self.user = try await self.apiClient.fetchUser()
user = try await apiClient.fetchUser()
} catch {}
}
}
Expand Down Expand Up @@ -200,7 +204,7 @@ This makes `FeatureModel`'s dependencies inherit from the parent feature, and yo
override any additional dependencies you want.

In general, if you want dependencies to be properly inherited through every layer of feature in your
application, you should make sure to create any `ObservableObject` models inside a
application, you should make sure to create any observable models inside a
``withDependencies(from:operation:file:line:)-8e74m`` scope.

If you do this, it also allows you to run previews in a very specific environment. Dependencies
Expand Down
Loading

0 comments on commit f12cc79

Please sign in to comment.