diff --git a/MVVM/MVVM.xcodeproj/project.pbxproj b/MVVM/MVVM.xcodeproj/project.pbxproj
index e2fcd1f..74ba8b4 100644
--- a/MVVM/MVVM.xcodeproj/project.pbxproj
+++ b/MVVM/MVVM.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		A569D7F1290BF6040053A48F /* Clocks in Frameworks */ = {isa = PBXBuildFile; productRef = A569D7F0290BF6040053A48F /* Clocks */; };
 		BD43014B27CFA04100EC0A07 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD43014A27CFA04100EC0A07 /* AppDelegate.swift */; };
 		BDC59D3827A839320054A19B /* CombineSchedulers in Frameworks */ = {isa = PBXBuildFile; productRef = BDC59D3727A839320054A19B /* CombineSchedulers */; };
 		BDF647B827BE381100EF94EF /* XCTestDynamicOverlay in Frameworks */ = {isa = PBXBuildFile; productRef = BDF647B727BE381100EF94EF /* XCTestDynamicOverlay */; };
@@ -26,6 +27,7 @@
 			files = (
 				BDF647B827BE381100EF94EF /* XCTestDynamicOverlay in Frameworks */,
 				BDC59D3827A839320054A19B /* CombineSchedulers in Frameworks */,
+				A569D7F1290BF6040053A48F /* Clocks in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -77,6 +79,7 @@
 			packageProductDependencies = (
 				BDC59D3727A839320054A19B /* CombineSchedulers */,
 				BDF647B727BE381100EF94EF /* XCTestDynamicOverlay */,
+				A569D7F0290BF6040053A48F /* Clocks */,
 			);
 			productName = MVVM;
 			productReference = BDC59D1F27A82C820054A19B /* MVVM.app */;
@@ -110,6 +113,7 @@
 			packageReferences = (
 				BDC59D3627A839320054A19B /* XCRemoteSwiftPackageReference "combine-schedulers" */,
 				BDF647B627BE381100EF94EF /* XCRemoteSwiftPackageReference "xctest-dynamic-overlay" */,
+				A569D7EF290BF6040053A48F /* XCRemoteSwiftPackageReference "swift-clocks" */,
 			);
 			productRefGroup = BDC59D2027A82C820054A19B /* Products */;
 			projectDirPath = "";
@@ -339,6 +343,14 @@
 /* End XCConfigurationList section */
 
 /* Begin XCRemoteSwiftPackageReference section */
+		A569D7EF290BF6040053A48F /* XCRemoteSwiftPackageReference "swift-clocks" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/pointfreeco/swift-clocks";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 0.1.4;
+			};
+		};
 		BDC59D3627A839320054A19B /* XCRemoteSwiftPackageReference "combine-schedulers" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "https://github.com/pointfreeco/combine-schedulers.git";
@@ -358,6 +370,11 @@
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
+		A569D7F0290BF6040053A48F /* Clocks */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = A569D7EF290BF6040053A48F /* XCRemoteSwiftPackageReference "swift-clocks" */;
+			productName = Clocks;
+		};
 		BDC59D3727A839320054A19B /* CombineSchedulers */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = BDC59D3627A839320054A19B /* XCRemoteSwiftPackageReference "combine-schedulers" */;
diff --git a/MVVM/MVVM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MVVM/MVVM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index cc1f7b9..7ea91a1 100644
--- a/MVVM/MVVM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/MVVM/MVVM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -9,13 +9,22 @@
         "version" : "0.5.3"
       }
     },
+    {
+      "identity" : "swift-clocks",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/pointfreeco/swift-clocks",
+      "state" : {
+        "revision" : "692ec4f5429a667bdd968c7260dfa2b23adfeffc",
+        "version" : "0.1.4"
+      }
+    },
     {
       "identity" : "xctest-dynamic-overlay",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
       "state" : {
-        "revision" : "50a70a9d3583fe228ce672e8923010c8df2deddd",
-        "version" : "0.2.1"
+        "revision" : "16e6409ee82e1b81390bdffbf217b9c08ab32784",
+        "version" : "0.5.0"
       }
     }
   ],
diff --git a/MVVM/mvvm.playground/Pages/Testing time based events.xcplaygroundpage/Contents.swift b/MVVM/mvvm.playground/Pages/Testing time based events.xcplaygroundpage/Contents.swift
new file mode 100644
index 0000000..d65c666
--- /dev/null
+++ b/MVVM/mvvm.playground/Pages/Testing time based events.xcplaygroundpage/Contents.swift	
@@ -0,0 +1,154 @@
+//: [Previous](@previous)
+
+import Combine
+import CombineSchedulers
+import Foundation
+import PlaygroundSupport
+import SwiftUI
+import XCTest
+import Clocks
+
+/// Let's suppose we have an Onboarding screen with a carousel that
+/// automatically shows the next view after a small delay of 5 seconds.
+class OnboardingViewModel: ObservableObject {
+    @Published var cards: [String]
+    @Published var currentIndex: Int
+    private let clock: any Clock<Duration>
+    private var task: Task<Void, Error>?
+    
+    var currentCard: String? {
+        guard currentIndex < cards.count else { return nil }
+        return cards[currentIndex]
+    }
+    
+    init(items: [String], clock: any Clock<Duration>) {
+        self.cards = items
+        self.currentIndex = 0
+        self.clock = clock
+    }
+    
+    func start() {
+        task = Task {
+            while true {
+                try await clock.sleep(for: .seconds(5))
+                currentIndex = (currentIndex + 1) % cards.count
+            }
+        }
+    }
+    
+    func stop() {
+        task?.cancel()
+        task = nil
+    }
+}
+
+// MARK: - Tests -
+class OnboardingViewModelTestCase: XCTestCase {
+    func testCarousel() async {
+        let items = ["One", "Two", "Three", "Four", "Five"]
+        let clock = TestClock()
+        let viewModel = OnboardingViewModel(items: items, clock: clock)
+        
+        // When the view model gets initialized it should show the first element
+        XCTAssertEqual(viewModel.currentCard, items[0])
+        
+        // Even if 50 seconds pass, it should still show the first element since
+        // we haven't started the carousel
+        await clock.advance(by: .seconds(50))
+        XCTAssertEqual(viewModel.currentCard, items[0])
+        
+        // After 2.5s the carousel we should still see the 1st element
+        viewModel.start()
+        await clock.advance(by: .seconds(2.5))
+        XCTAssertEqual(viewModel.currentCard, items[0])
+        
+        // But after further 2.6 (5.1s) we should see the 2nd element
+        await clock.advance(by: .seconds(2.6))
+        XCTAssertEqual(viewModel.currentCard, items[1])
+        
+        // 15 seconds later we should see the last element
+        await clock.advance(by: .seconds(15))
+        XCTAssertEqual(viewModel.currentCard, items[4])
+        
+        // 10 seconds later it should go back to the 2nd element
+        await clock.advance(by: .seconds(10))
+        XCTAssertEqual(viewModel.currentCard, items[1])
+        
+        // After we stop the carousel, it should still show the 2nd element
+        viewModel.stop()
+        await clock.advance(by: .seconds(15))
+        XCTAssertEqual(viewModel.currentCard, items[1])
+        await clock.advance(by: .seconds(5))
+        XCTAssertEqual(viewModel.currentCard, items[1])
+        
+        // After resuming the carousel it should show the same element as before
+        viewModel.start()
+        
+        XCTAssertEqual(viewModel.currentCard, items[1])
+        await clock.advance(by: .seconds(6.5))
+        XCTAssertEqual(viewModel.currentCard, items[2])
+    }
+}
+
+// And run the tests
+OnboardingViewModelTestCase.defaultTestSuite.run()
+
+class FeatureViewModel: ObservableObject {
+    @Published var isButtonDisabled = true
+    @Published var showTopView = false
+    private let clock: any Clock<Duration>
+    
+    init(clock: any Clock<Duration>) {
+        self.clock = clock
+    }
+    
+    @Sendable func onViewAppear() async {
+        do {
+            try await clock.sleep(for: .seconds(3))
+            isButtonDisabled = false
+        } catch { print(error) }
+    }
+}
+
+// Here we have a view that has a button that is disabled
+// for the first 3 seconds. Maybe we want to show the user
+// a short video before allowing them to click the button.
+struct FeatureView: View {
+    @ObservedObject var viewModel: FeatureViewModel
+    
+    var body: some View {
+        VStack {
+            if viewModel.showTopView {
+                Image(systemName: "car")
+                    .resizable()
+                    .scaledToFit()
+                    .frame(width: 50, height: 50)
+            }
+            
+            Spacer()
+            Button {
+                viewModel.showTopView.toggle()
+            } label: {
+                Text(viewModel.showTopView ? "Hide" : "Show")
+            }
+            .disabled(viewModel.isButtonDisabled)
+        }
+        .padding(40)
+        .task(viewModel.onViewAppear)
+    }
+}
+
+// If we want to quickly iterate on the design of this view with previews,
+// using a ContinuousClock() we would have to wait 3 seconds before
+// being able to click the button. To solve this issue we can use
+// ImmediateClocks. This type of clock "removes" all delays when sleeping tasks.
+// This can also be use in unit testing but TestClock is more useful.
+PlaygroundPage.current.setLiveView(
+    FeatureView(
+        viewModel: FeatureViewModel(clock: ImmediateClock())
+//        viewModel: FeatureViewModel(clock: ContinuousClock())
+    )
+    .frame(width: 200, height: 200)
+)
+
+//: [Next](@next)
diff --git a/MVVM/mvvm.playground/contents.xcplayground b/MVVM/mvvm.playground/contents.xcplayground
index d5fba92..cf042f1 100644
--- a/MVVM/mvvm.playground/contents.xcplayground
+++ b/MVVM/mvvm.playground/contents.xcplayground
@@ -7,5 +7,6 @@
         <page name='Advanced dependencies'/>
         <page name='[WIP]Mixing VIPER and MVVM'/>
         <page name='[WIP]Testing VIPER, easy edition'/>
+        <page name='Testing time based events'/>
     </pages>
 </playground>
\ No newline at end of file