Video #209: Clocks: Existential Time
Episode: Video #209 Date: Oct 17, 2022 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep209-clocks-existential-time

Description
The Clock protocol is a brand-new feature of Swift 5.7 for dealing with time-based asynchrony. We will explore its interface, compare it to Combine’s Scheduler profile, and see what it takes to write and use our own conformances.
Video
Cloudflare Stream video ID: b48b6e84912c922b9386e4d61b526d7a Local file: video_209_clocks-existential-time.mp4 *(download with --video 209)*
References
- Discussions
- async-algorithms
- suggest
- started a pitch on Swift evolution
- XCTestDynamicOverlay
- SE-0329: Clock, Instant, and Duration
- SE-0374: Add
sleep(for:)toClock - 0209-clocks-pt1
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
Swift 5.7 was released a few months ago, and with it came a new tool for dealing with time-based asynchrony, but it has mostly flown under the radar so far. I’m talking about the Clock protocol, which encapsulates the idea of suspending for an amount of time in an asynchronous context.
— 0:21
This concept is similar to schedulers that are native to the Combine framework. Schedulers encapsulates the idea of how to schedule work at a future date, or even repeatedly on an interval. Schedulers pair well with reactive programming frameworks such as Combine because they can not only express time-based asynchrony, but can also describe the execution context of where to perform work or deliver values, such as on a particular dispatch queue, operation queue or run loop.
— 0:48
But clocks operate in Swift’s new structured concurrency model, and so they can be a lot simpler than schedulers. They don’t need to describe how or where they are going to perform work. They can simply use a surrounding asynchronous context to suspend for an amount of time, and then once that time passes it will allow execution to continue flowing in its task. No need to think about threads or queues.
— 1:11
So, it’s great that clocks are simpler than schedulers, and of course it would be great if we could slowly replace any usages of schedulers with clocks in order to remove yet another dependency on the Combine framework. But, if we’re not careful, we will accidentally introduce time-based asynchrony into our features that is completely uncontrollable, making it harder to iterate on our features and harder, if not impossible, to test our features.
— 1:35
So, this episode we are going to explore the new Clock protocol to see why it’s so special, and then see what needs to be done so that we can take control of time rather than having it control us. The Clock protocol
— 1:50
Let’s start by taking a look at the clock protocol: public protocol Clock<Duration>: Sendable { associatedtype Duration where Self.Duration == Self.Instant.Duration associatedtype Instant: InstantProtocol var now: Self.Instant { get } var minimumResolution: Self.Duration { get } func sleep( until deadline: Self.Instant, tolerance: Self.Instant.Duration? ) async throws }
— 1:53
It looks quite advanced, but the core idea is quite simple. It’s just leveraging a few advanced features of Swift in order to make its usage more ergonomic.
— 2:03
First we can see that the Clock protocol inherits from the Sendable protocol, so any conformances must be safe to use from concurrent contexts. And this makes sense because you can only sleep in asynchronous contexts, and should be able to sleep from any number of concurrently running tasks.
— 2:20
Next, the protocol has two associated types, one for a Duration and one for an Instant , but there’s a constraint that forces the Duration to match the Duration associated type inside the Instant . So, the Duration isn’t a free associated type. It has no freedom to be whatever it wants. It must be equal to the instant’s duration.
— 2:44
That seems weird. Why even have an associated type in that case? Well, the reason is that it can be exposed as a primary associated type, which we can see is being used in the angle brackets: public protocol Clock<Duration>: Sendable { … }
— 2:58
So, that is why a seemingly superfluous associated type is added to the protocol, but doesn’t explain why this protocol wants to have a primary associated type. We will explore that more in a moment.
— 3:09
And now that we know that the protocol has associated types, what do they mean?
— 3:13
The Instant associated type conforms to the InstantProtocol , which generalizes the concept of an absolute moment of time. We are all used to using Foundation’s Date type to represent an absolute moment in time, and so the InstantProtocol serves a similar purpose, but it exists in the standard library rather than Foundation.
— 3:33
We can jump to the definition of the InstantProtocol to see what kind of functionality it exposes: public protocol InstantProtocol<Duration>: Comparable, Hashable, Sendable { associatedtype Duration: DurationProtocol func advanced(by duration: Self.Duration) -> Self func duration(to other: Self) -> Self.Duration }
— 3:34
It has an associated type, Duration , that conforms to the DurationProtocol , and that associated type is primary.
— 3:42
Durations are relative instants, or deltas between instants. That is, if you subtract two instants you get a duration. And if you want to move an instant forward to another instant, you must add a duration.
— 3:55
The DurationProtocol looks like this: public protocol DurationProtocol: AdditiveArithmetic, Comparable, Sendable { static func / (lhs: Self, rhs: Int) -> Self static func /= (lhs: inout Self, rhs: Int) static func * (lhs: Self, rhs: Int) -> Self static func *= (lhs: inout Self, rhs: Int) static func / (lhs: Self, rhs: Self) -> Double }
— 4:09
It just gives you the ability to scale by integer multipliers. This makes it easy to divide a duration into segments or scale a duration by an integer.
— 4:21
The other functionality in the InstantProtocol allows you to add a duration to an instant to get another instant: func advanced(by duration: Self.Duration) -> Self
— 4:39
And allows you to subtract two instants to get a duration: func duration(to other: Self) -> Self.Duration
— 4:50
It’s worth nothing that these associated types are analogous to the Scheduler protocol’s SchedulerTimeType associated type: public protocol Scheduler<SchedulerTimeType> { associatedtype SchedulerTimeType: Strideable where Self.SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible … }
— 5:01
The SchedulerTimeType is like the instant, and then its stride is like the duration. The scheduler protocol is a little less general here because the stride is forced to conform to the SchedulerTimeIntervalConvertible protocol, which restricts it to be a SI unit, such as seconds, milliseconds, microseconds or nanoseconds. Instants and durations have no such constraints and so are more general.
— 5:40
And now that we know about all of that surrounding context, we can finally take a look at the actual requirements of the Clock protocol: var now: Self.Instant { get } var minimumResolution: Self.Duration { get } func sleep( until deadline: Self.Instant, tolerance: Self.Instant.Duration? ) async throws
— 5:51
The first requirement is that any Clock conformance must expose a now instant that represents the current instant of the clock: var now: Self.Instant { get }
— 6:03
Next is minimumResolution , which is the smallest measure amount of time that can be measured between two instants: var minimumResolution: Self.Instant.Duration { get }
— 6:12
And finally, the real meat of the protocol, is its sleep method: func sleep( until deadline: Self.Instant, tolerance: Self.Instant.Duration? ) async throws
— 6:16
This is the async method that every conformance must provide in order to suspend to a future instant. It behaves similarly to the Task.sleep method that we have probably all used, except it specifies the clock to use to do the sleeping, whereas Task.sleep doesn’t use a clock at all. Under the hood it just schedules its work directly with the operating system by calling out to C++ code.
— 6:40
So, that’s the Clock protocol. It is very similar to the Scheduler protocol from Combine. For example, schedulers also have the concept of “now” and “minimum resolution”, though they call it “minimum tolerance”: var now: Self.SchedulerTimeType { get } var minimumTolerance: Self.SchedulerTimeType.Stride { get }
— 6:55
But, the Clock protocol is quite a bit simpler than the Scheduler protocol. For example, rather than having a single endpoint for expressing the idea of scheduling work, it has 3: func schedule( options: Self.SchedulerOptions?, _ action: @escaping () -> Void ) func schedule( after date: Self.SchedulerTimeType, tolerance: Self.SchedulerTimeType.Stride, options: Self.SchedulerOptions?, _ action: @escaping () -> Void ) func schedule( after date: Self.SchedulerTimeType, interval: Self.SchedulerTimeType.Stride, tolerance: Self.SchedulerTimeType.Stride, options: Self.SchedulerOptions?, _ action: @escaping () -> Void ) -> Cancellable
— 7:05
The first schedules a unit of work as soon as possible. The second schedules a unit of work at a future time. And the third schedules a unit of work to be run repeatedly on an interval.
— 7:10
Each of these types of scheduling are quite different, and so it’s no surprise we need a few endpoints to accomplish it. But, what may be surprising is that the Clock ’s single endpoint is capable of expressing all of these things, which means clocks are inherently simpler and a more foundational concept.
— 7:30
Let’s actually get our hands dirty now and play around with some clocks. We’ll hop over to a playground to explore the APIs.
— 7:37
First of all, we can fire up a new task since playgrounds don’t really support async code at the root, and in that task do a sleep for a second in the traditional style: Task { try await Task.sleep(for: .seconds(1)) }
— 8:04
And just to make sure it is really sleeping let’s measure the time before and after the sleep: Task { let start = DispatchTime.now().uptimeNanoseconds try await Task.sleep(for: .seconds(1)) print( "Duration", DispatchTime.now().uptimeNanoseconds - start ) } Duration 1098292666
— 8:28
We can see that indeed about a second passed during the sleep. It’s not exactly a second, but it’s close.
— 8:38
Now currently, this invocation of sleep doesn’t involve any clocks. As we mentioned a moment ago, under the hood it just schedules its work directly with the operating system by calling out to C++ code.
— 8:50
In order to involve an actual clock we need to somehow construct one. The standard library ships with two concrete conformances, known as SuspendingClock and ContinuousClock . Foundation is supposed to also ship with a conformance known as a UTCClock , but that hasn’t shipped with Xcode yet. Seems like something is holding up the implementation.
— 9:09
A suspending clock is one whose internal time stops when the system falls asleep, and then resume once the system wakes up. So if you sleep a task for a minute, and in the middle of that task you close your computer and come back hours later, the task will resume where it left off, waiting a little more time until the full minute finishes.
— 9:38
We can construct a suspending clock and sleep with it like so: Task { let clock = SuspendingClock() let duration = try await clock.measure { try await clock.sleep( until: clock.now.advanced(by: .seconds(1)) ) } print("Suspending Duration", duration) } Suspending Duration 1.064123416 seconds
— 10:26
This code looks about the same and works about the same, but we are now referring to an explicit clock.
— 10:36
The continuous clock is one that does not pause when the system falls asleep. So, if you sleep a task for a minute with a continuous clock, and in the middle close your computer and come back hours later, when you open your computer the sleep will immediately end because its minute duration ended long ago.
— 10:56
We can construct a clock and sleep with it in much the same was as a suspending clock: Task { let clock = ContinuousClock() let duration = try await clock.measure { try await clock.sleep( until: clock.now.advanced(by: .seconds(1)) ) } print("Continuous Duration", duration) } Continuous Duration 1.0529495 seconds
— 11:12
Now, the sleep tool may seem incredibly crude, especially when compared with schedulers, which had 3 methods for scheduling work: immediately, at a deadline, and repeatedly on an interval.
— 11:25
However, we can recover all 3 of those types of scheduling using sleep . The first style of scheduling work, where you do so immediately, comes for free thanks to how structured concurrency works. If you want to execute work as quickly as possible with a clock you simply do not sleep with the clock and just do your work. Nothing special is needed.
— 11:48
The second style, where you perform some work after a duration of time passes, is also quite straightforward. You can simply sleep and then perform your work.
— 12:00
The third style, where you perform work repeatedly on an interval, is a little more interesting. To capture this with a clock we can simply enter an infinite loop and perform the sleep inside the loop: Task { let clock = SuspendingClock() while true { try await clock.sleep( until: clock.now.advanced(by: .seconds(1)) ) print("Timer tick") } } Timer tick Timer tick Timer tick Timer tick Timer tick …
— 12:37
So, that’s incredibly easy. No need for a separate operation when it’s that easy.
— 12:47
However, although it’s easy, it’s also crude enough that you may not want to use this for every use case. Sleeping with clocks is not super precise, as we saw a moment ago where sleeping for one second actually took 1.07 seconds.
— 13:03
Those small imprecisions can add up over time, which we can see by measure how much time has elapsed with each tick of our timer: Task { let clock = SuspendingClock() var start = clock.now while true { try await clock.sleep( until: .now.advanced(by: .seconds(1)) ) print("Tick", clock.now - start) } } Tick 1.070236459 seconds Tick 2.138239417 seconds … Tick 23.958566375 seconds Tick 25.014811459 seconds
— 13:44
So after just 25 ticks we have drifted a full second off our interval.
— 13:48
If precision is important we would need to layer on a lot more logic on top of this to sleep in order to adjust the amount we sleep based on how much the timer has drifted. That’s easy enough to do, and Apple even has an open source library called async-algorithms that provides an async sequence for timers, among other tools.
— 14:10
But, what’s really cool about having a timer based on sleeps is that we can be very dynamic with how much time we sleep. Like what if we wanted a timer that started off fast, but got slower and slower as time went on. This is really easy to do: Task { let clock = SuspendingClock() var start = clock.now var count = 0 while true { count += 1 try await clock.sleep( until: clock.now.advanced(by: .milliseconds(count)) ) print("Tick", clock.now - start) } }
— 15:03
This would be quite difficult to accomplish with the Scheduler API. You would need to define a function so that you could recursively call it from inside a schedule block. It’s certainly possible, it’s just not as natural as when using clocks and structured concurrency.
— 15:21
And not just timers can be created with clocks. You can also recreate all of the time-based operators that we know and love from Combine, such as throttle, debounce, timeouts, and more. Some of those operators are also included in Apple’s async-algorithms package. Injecting clocks
— 15:35
So, we now understand the basics of clocks and how they work. But the question is… why use a clock? So far, is invoking sleep on a clock versus invoking sleep as a static method on Task all that much different? I guess one difference is choosing between a suspending or continuous clock, but that also seems like a pretty narrow use case to actually care about.
— 15:57
Well, the real reason to use clocks is that we get a chance to define our own new clocks that allow us to control time rather than us being at the whims of the endless march of time. There are many occasions where it is not appropriate to literally wait for time to pass in order for later code to execute, such as in tests and SwiftUI previews.
— 16:15
We explored this concept extensively when we covered schedulers on Point-Free, over 2 years ago. In those episodes we saw there were a lot of benefits in defining things such as “immediate” schedulers and “test” schedulers.
— 16:28
So, let’s repeat that program for clocks. To motivate this, let’s consider a simple SwiftUI view that makes use of time-based asynchrony.
— 16:40
Imagine you have a SwiftUI view that updates its UI 5 seconds after the view first appears. Just to keep things simple, let’s show a simple welcome message after 5 seconds. That is quite easy to do in SwiftUI thanks to the new .task view modifier that is called when a view appears. struct ContentView: View { @State var message = "" var body: some View { VStack { Text(message) } .task { do { try await Task.sleep(for: .seconds(5)) withAnimation { self.message = "Welcome!" } } catch {} } } }
— 18:08
That’s all it takes.
— 18:11
We can run this in a preview to see that it does indeed work how we expect. We have to wait 5 seconds, but eventually the welcome message appears.
— 18:20
The problem is that we have to literally wait for 5 seconds to pass before the message appears. That may not seem like a problem, after all that is the behavior of the feature, but what if we want to start making edits to this screen? What if we want to update the font of the welcome message: Text(message) .font(.title)
— 18:39
We now have to wait another 5 seconds to pass to see what this looks like.
— 18:43
Next we may want to update the text color: Text(message) .font(.title) .foregroundColor(.mint)
— 18:53
Again we have to wait 5 seconds.
— 18:58
This has completely destroyed our ability to quickly iterate on this feature. Every little change we make comes with a 5 second penalty just to see what is going on. It pretty much defeats the purpose of using a SwiftUI preview in the first place.
— 19:10
And this problem doesn’t just affect people who like to use SwiftUI previews. If you write tests for your features you will come across similar problems anytime your feature needs access to time-based asynchrony.
— 19:21
To see this, let’s quickly extract the core logic and behavior of this feature into an observable object: @MainActor class FeatureModel: ObservableObject { @Published var message = "" func task() async { do { try await Task.sleep(for: .seconds(5)) withAnimation { self.message = "Welcome!" } } catch {} } }
— 19:42
And then the view can make use of this model rather than executing the logic directly in the view: struct ContentView: View { @ObservedObject var model: FeatureModel var body: some View { VStack { Text(self.model.message) .font(.title) .foregroundColor(.mint) } .task { await self.model.task() } } }
— 20:25
The benefit to doing this is that it is quite easy to write a test for the FeatureModel , but quite difficult to do the same with a view. You really have no choice but to load your view up in a UI test, which can be slow and flakey, making them difficult to trust.
— 20:43
However, while models are easier to test, it doesn’t mean it’s easy to test. Let’s try writing a test that exercises the behavior that 5 seconds after the feature first appears a welcome message is shown: @MainActor final class ClockExplorationTests: XCTestCase { func testWelcome() async { let model = FeatureModel() XCTAssertEqual(model.message, "") await model.task() XCTAssertEqual(model.message, "Welcome!") } }
— 21:44
So, it was quite easy to write this test, and if we run it, it does pass. But it took a really long time: Test Suite 'ClocksExplorationTests' passed. Executed 1 test, with 0 failures (0 unexpected) in 5.059 (5.060) seconds We have no choice but to literally wait for 5 seconds to pass in order for the view model to do its job.
— 22:01
This is completely unacceptable. This is going to make your test suite take much longer to execute than necessary, which slows down your productivity and racks up time on CI servers, costing you actual money to run slow tests. Not to even mention that someday you may need to sleep for a much longer duration than just a few seconds. Suppose you want something to happen in your application 10 minutes after it is launched. In order to test that behavior are you really going to have a test case wait around for 10 minutes?
— 22:28
So, this just is not the way. What we need is the ability to use a live clock, like a continuous or suspending one, in production, but with the option of using a simpler, more controllable clock in tests and previews. In order for that to be possible we need to hold onto a clock directly in our feature so that a live one can be passed when running on a device, but a controlled one can be passed in for tests.
— 22:50
You might think you can simply do this: @MainActor class FeatureModel: ObservableObject { @Published var message: String? let clock: Clock init(clock: Clock) { self.clock = clock } … } Use of protocol ‘Clock’ as a type must be written ‘any Clock’
— 23:04
This doesn’t work in Swift 5.7 because you are no longer allowed to use bare protocols in this fashion. There is a very good reason for this, and it’s a topic we went deep into in our series on async Composable Architecture . In short, using protocols as types represent a vastly different thing than the other concrete types we are used to, such as integers, strings and booleans. They are so different that they warrant a separate name, known as existential types, and a separate syntax, the all lowercase any keyword: @MainActor class FeatureModel: ObservableObject { @Published var message: String? let clock: any Clock init(clock: any Clock) { self.clock = clock } … }
— 23:44
That gets the FeatureModel class compiling, but we need to fix any place we construct a model since it now needs to be passed a clock. Since the model takes literally any kind of clock, we can pass a continuous one. We can do this for both the preview and the entry point of the application: ContentView( model: FeatureModel(clock: ContinuousClock()) )
— 24:09
And then theoretically we create our own Clock conformances and pass them to the FeatureModel in various situations. But, unfortunately that’s not really true right now.
— 24:18
To see this, let’s try actually using the clock held in the FeatureModel . Currently it’s not being used at all, and instead we are still calling out to the global, Task.sleep static method.
— 24:28
If we autocomplete the sleep method on the clock we will see what needs to be provided: self.clock.sleep( until: <#Clock.Instant#>, tolerance: <#Clock.Instant.Duration?#> )
— 24:37
We need to supply the instant that represents a future moment in time that we want to sleep to. The problem is that the clock we are holding onto is fully type erased: let clock: any Clock
— 24:50
This means everything about the associated types has been forgotten, including the instant and duration. There isn’t much we can do to construct instances.
— 24:59
Now, every clock has a now property, which is an instant, so you may think you can just pluck that off the clock: let instant = self.clock.now Inferred result type ‘any InstantProtocol’ requires explicit coercion due to loss of generic requirements
— 25:09
But even that is not possible because the Instant associated type is so fully erased. You are forced to tell the compiler that you understand that all you have here is a type erased instant by casting it: let instant = self.clock.now as any InstantProtocol
— 25:25
But this instant value isn’t very useful either. Because it is fully type erased, and in particular does not retain any information about its Duration , there’s really nothing you an do with it.
— 25:36
You might think you can advance it, but without knowing anything about the duration it’s not really possible to construct a duration: instant.advanced(by: .seconds(5)) Type ‘(any InstantProtocol).Duration’ has no member ‘seconds’
— 25:55
The only immediately constructible duration is the .zero duration, but even that doesn’t work: instant.advanced(by: .zero) Member ‘advanced’ cannot be used on value of type ‘any InstantProtocol’; consider using a generic constraint instead
— 26:05
Again, because the Duration associated type has been lost, the compiler has no idea how to execute this code.
— 26:11
One potential fix to this is to preserve all type information by introducing generics. So, rather than erasing types using existentials, we can make our FeatureModel generic over the type of clock used on the inside: @MainActor class FeatureModel<C: Clock>: ObservableObject { @Published var message: String? let clock: C init(clock: C) { self.clock = clock } … }
— 26:47
But even then we have a similar problem, except now our clock is so generic that we literally know nothing about the duration, and hence we don’t even know if the duration is capable of representing the concept of “5 seconds”: self.clock.now.advanced(by: .seconds(5)) Type ‘C.Duration’ has no member ‘seconds’
— 27:01
So, we need to slightly constrain the clock so that it isn’t truly any clock, but rather one that has a sensible duration that we know how to use, such as the Duration that ships with the standard library and is used by the ContinuousClock and SuspendingClock : @MainActor class FeatureModel<C: Clock>: ObservableObject where C.Duration == Swift.Duration { … }
— 27:30
Now we are finally able to abstractly compute an advanced instant: self.clock.now.advanced(by: .seconds(5))
— 27:35
Which means we can finally actually sleep with the clock: func task() async { do { try await self.clock.sleep( until: self.clock.now.advanced(by: .seconds(5)), tolerance: nil ) self.message = "Welcome!" } catch {} }
— 27:55
The FeatureModel is finally compiling, and it is now capable of being passed a ContinuousClock for the times we are running the feature on a device, but in tests or previews we can hand it some other kind of clock that handles the flow of time in a more appropriate way.
— 28:09
So, you may think this is the way to go, but there are a few compiler errors we have to fix, and fixing them is going to start to expose some really strange things. For example, first thing in the view we are met with a compiler error: struct ContentView: View { @ObservedObject var model: FeatureModel … } Reference to generic type ‘FeatureModel’ requires arguments in <…>
— 28:20
Now that the FeatureModel is generic we aren’t allowed to use it in a bare fashion like this. We have to specify a clock here. And remember, one of the main reasons of injecting a clock into our feature is so that we could control the clock in previews, which needs to load up this view. That means if we try to quiet the compiler here by just plugging in a concrete clock, like say a continuous clock: struct ContentView: View { @ObservedObject var model: FeatureModel<ContinuousClock> … }
— 28:43
…then we won’t get any of the benefits in our previews. We will still have to wait for a full 5 seconds of real life time to pass before we see the welcoming message.
— 28:55
Really we have no choice but to make our view generic over the type of clock used: struct ContentView<C: Clock>: View where C.Duration == Duration { @ObservedObject var model: FeatureModel<C> … }
— 29:15
Now the view compiles, but this is really strange. First of all, this loudly announces to the world that this view statically depends on a clock, but why would anyone care about that. Is there some kind of abstract, generic algorithm we are going to write on ContentView s that depends on the type of clock used on the inside? Of course not. In fact, the generic will even be fixed for the entire lifetime of the application. It has no way to change midway, like suddenly going from a continuous clock to suspending clock.
— 29:45
Even worse, this generic is going to start to infect every single piece of code that touches the model or the view. If some view embeds this view, then that view better also pick up a generic on a clock if it wants to have the benefits of not having to wait for real time to pass in order to see its preview. Same goes for another observable object that wants to interact with the FeatureModel . It will also need a generic.
— 30:06
In short, generics are not the right tool for abstraction here. The kind of clock our feature is using is just an internal implementation detail, and does not need to be announced to the world. So, let’s undo that and go back to the any Clock we had a moment ago.
— 30:21
The correct tool we want for this is the opposite of a generic, also known as an existential. We are already using an existential, that’s what any Clock is, but it is an underpowered existential. It has erased too much information to be usable.
— 30:37
What we need to do is leverage primary associated types in order to allow erasing the clock type for the most part while, retaining some information about the associated types. The Clock API nearly shipped without a primary associated type, which means clock existentials would have been impossible to use, and we would be forced to construct an ad hoc AnyClock type eraser to make it actually usable. But, luckily we saw this shortcoming while working on this very episode, and we were able to suggest to the core team that Clock have a primary associated type.
— 31:07
The primary associated type it exposes is the Duration . This is because the duration is the most foundational measurement for interacting with a clock, as evident by the fact that all concrete clocks in the standard library share the same Duration type but each define their own Instant . This is because it’s not appropriate for us to mix and match and compare a continuous clock’s instant with a suspending clock’s instant. They are two completely different measurements with a different point of origin. But, their relative measurements, that is the delta between two instants, are of a common type, and that is the Duration .
— 31:38
This is similar to how things work for the Sequence protocol too. That protocol also has two associated types, one for the Element and one for the Iterator , even though the Element is fully determined by the iterator: public protocol Sequence<Element> { associatedtype Element where Self.Element == Self.Iterator.Element associatedtype Iterator: IteratorProtocol … }
— 32:05
It does this specifically so that it can make the Element the primary associated type, allowing you to do things like any Sequence<Int> . This is because the element is the most important piece of information for a sequence, and the iterator is just an internal implementation detail. If the iterator had been made the primary associated type you would need to do something like: any Sequence<any Iterator<Int>>
— 32:32
…in order to express something that should be very simple.
— 32:37
So, our FeatureModel shouldn’t hold onto literally any clock, but rather one whose duration matches the Duration that ships with the standard library: @MainActor class FeatureModel: ObservableObject { @Published var message: String? let clock: any Clock<Duration> init(clock: any Clock<Duration>) { self.clock = clock } … }
— 32:52
And that’s because Duration is concrete, is used by ContinuousClock and SuspendingClock , and makes it easy to construct values in terms of seconds, milliseconds, and so on.
— 33:00
So, it finally seems like we are on the right track, and it would feel really good to finally get a win after all the false starts we’ve had. But unfortunately life is really messy.
— 33:10
The problem is that the sleep method on clocks still works only with instants: self.clock.sleep( until: <#Clock<Duration>.Instant#>, tolerance: <#Clock<Duration>.Instant.Duration?#> )
— 33:15
…and our clock has fully erased the notion of instant. We have preserved the notion of duration, and that’s handy and what we really want, but the clock APIs are forcing us to think in terms of instants.
— 33:29
This just seems like an oversight in the design of the Clock protocol. There should be a way to sleep with a clock by specifying only the duration you want to sleep, rather than the future instant you want to sleep to. This is evident by the fact that there is a duration-based sleep static method on Task : try await Task.sleep(for: .seconds(5))
— 33:51
So, if is a duration-based sleep method on Task , why isn’t there one on clocks? We can define one quite easily ourselves: extension Clock { /// Suspends for the given duration. public func sleep( for duration: Duration, tolerance: Duration? = nil ) async throws { try await self.sleep( until: self.now.advanced(by: duration), tolerance: tolerance ) } }
— 34:12
In fact, we started a pitch on Swift evolution to introduce this very method to the standard library, and so hopefully in a near future Swift we won’t have to define this API ourselves.
— 34:21
Now that we have an API that deals only with durations instead of instants, we can finally sleep with an existential clock: func task() async { do { try await self.clock.sleep(for: .seconds(5)) self.message = "Welcome!" } catch {} } Immediate clocks
— 34:53
This now compiles, and we are finally in a position to swap out clocks in the FeatureModel whenever we want. We can boot up the application using a real life clock, such as the ContinuousClock , but then in previews and tests we can swap in something else.
— 35:06
So, that was a lot of work to just get us into a position where we can make use of other kinds of clocks that don’t track one-to-one with the flow of real world time, but we still have yet to actually construct another clock.
— 35:19
We need to create our own type that conforms to the Clock protocol so that we can provide new kinds of sleeping behavior, and not be at the whims of the real world.
— 35:28
Let’s start with the simplest kind of custom clock. One that squashes all of time into a single moment so that if you tell it to sleep, it just ignores you and execution of your asynchronous context keeps marching right on. It’s appropriate for the times that you don’t care about testing the specifics of some time-based asynchrony. You just don’t want to wait around for time to pass and want to get on with things.
— 35:53
We will call such a clock an “immediate” clock: public struct ImmediateClock: Clock { }
— 36:02
If we let the compiler fill in some requirements for our new type we will see that we have some associated types we need to account for: struct ImmediateClock: Clock { typealias Duration = <#type#> typealias Instant = <#type#> }
— 36:11
The duration can just be the type that ships with the standard library since it’s the most common type of duration and gives us an easy way to construct its values using SI units such as seconds and milliseconds: struct ImmediateClock: Clock { typealias Duration = Swift.Duration typealias Instant = <#type#> }
— 36:30
The Instant will be a new custom type that is defined directly inside the ImmediateClock type. This follows the pattern set before us by ContinuousClock and SuspendingClock , where they share the duration but define their own instants: struct ImmediateClock: Clock { typealias Duration = Swift.Duration struct Instant: InstantProtocol { } }
— 37:00
To conform to the InstantProtocol we need to specify the type of duration, which as we’ve said before we will take from the standard library: struct Instant: InstantProtocol { typealias Duration = Swift.Duration }
— 37:11
And then there are a bunch of requirements we need to implement, such as advancing an instant by a duration, finding the difference of two instants, and comparing two instants: struct Instant: InstantProtocol { typealias Duration = Swift.Duration func advanced(by duration: Duration) -> Self { <#code#> } func duration(to other: Self) -> Duration { <#code#> } static func < (lhs: Self, rhs: Self) -> Bool { <#code#> } }
— 37:29
To implement these requirements we will hold onto a private duration inside the instant to represent the distance from the current instant to zero: struct Instant: InstantProtocol { private var offset: Duration = .zero … }
— 37:39
So, even though the instant is an absolute measurement and duration is a relative one, we can still define one in terms of the other.
— 37:56
And now the requirements are quite straightforward to implement: struct Instant: InstantProtocol { private var offset: Duration = .zero func advanced(by duration: Duration) -> Self { .init(offset: self.offset + duration) } func duration(to other: Self) -> Duration { other.offset - self.offset } static func < (lhs: Self, rhs: Self) -> Bool { lhs.offset < rhs.offset } }
— 38:24
Now that we’ve got the associated types in place we can start working on the real requirements of the Clock protocol, starting with the now and minimumResolution properties. These concepts aren’t particularly interesting for a clock that never does any actual suspending and instead collapses all of time into a single instant, so we can just give them the most basic defaults: var now = Instant() let minimumResolution = Duration.zero
— 39:18
And finally we need to implement the sleep method: func sleep( until deadline: Instant, tolerance: Duration? ) async throws { <#code#> }
— 39:22
This is a funny one to implement. The purpose of the immediate clock is to not suspend at all if someone asks it to sleep. So, we can just do nothing in this method: func sleep( until deadline: Instant, tolerance: Duration? ) async throws { }
— 39:31
It may seem anti-climatic, but that’s all it takes to create an immediate clock, and this fixes all of the problems we were seeing earlier. Now technically we should probably move the now value forward to the deadline, but that’s not really needed for our test so we won’t worry about that right now.
— 39:56
For example, in our Xcode preview we can now use an immediate clock rather than a continuous clock: struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView( model: FeatureModel(clock: ImmediateClock()) ) } }
— 40:09
…and now our welcome message appears immediately. No need to wait for 5 seconds for it to show.
— 40:18
This means we can now iterate on the design in a super fast cycle, saving 5 seconds for every single little tweak we want to make. For example, say we want to bump the font size and bold the text: Text(message) .font(.largeTitle.bold())
— 40:35
We can see those changes instantly. Or, say we want to change the foreground color to a gradient style: Text(message) .font(.largeTitle.bold()) .foregroundStyle( LinearGradient( colors: [.mint, .yellow, .orange], startPoint: .leading, endPoint: .trailing ) )
— 40:47
Again, this shows immediately. This is a huge improvement to the quality of life for working on this feature.
— 41:01
The same goes for tests too. Why use a live, continuous clock in tests when we can use an immediate clock: func testWelcome() async { let model = FeatureModel(clock: ImmediateClock()) XCTAssertEqual(model.message, nil) await model.task() XCTAssertEqual(model.message, "Welcome!") }
— 41:18
Now tests pass in a tiny fraction of second: Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.002) seconds
— 41:26
No need to hold up the test suite for 5 seconds just so that the model can get past the clock suspension and execute its logic.
— 41:34
Heck, we can run this single test 5,000 times in less time than it took to run the original test when using the continuous clock: Executed 5000 tests, with 0 failures (0 unexpected) in 2.956 (3.876) seconds
— 41:49
This means that theoretically we could even write property-based tests for our features that use asynchrony since everything runs so quickly. Unimplemented clocks
— 41:57
Immediate clocks aren’t the only kind of clock that can be useful. It may sound weird, but sometimes it can be handy to have a clock that acts similar to immediate clocks, in that it doesn’t actually ever suspend, but it further performs an XCTFail whenever its sleep method is invoked.
— 42:14
If you were to use such a clock in your feature when writing tests you would have definitive proof that that dependency is never even used in the execution flow you are testing. That can be incredibly powerful.
— 42:26
Let’s quickly add some new behavior to our FeatureModel to show why this can be handy. Suppose that the model also manage some mutable count state, and exposes some endpoints for incrementing, decrementing and computing the nth prime based on the current count: @MainActor class FeatureModel: ObservableObject { @Published var count = 0 init( clock: any Clock<Duration>, count: Int = 0 ) { self.clock = clock self.count = count } … func incrementButtonTapped() { self.count += 1 } func decrementButtonTapped() { self.count -= 1 } func nthPrimeButtonTapped() async { var primeCount = 0 var prime = 2 while primeCount < self.count { defer { prime += 1 } if isPrime(prime) { primeCount += 1 } else if prime.isMultiple(of: 1_000) { await Task.yield() } } self.message = "\(self.count)th prime is \(prime - 1)" } private func isPrime(_ p: Int) -> Bool { if p <= 1 { return false } if p <= 3 { return true } for i in 2...Int(sqrtf(Float(p))) { if p % i == 0 { return false } } return true } }
— 43:03
Further, the nth prime computation can be quite intense, especially for very large numbers, so we’ve done some upfront work to make sure it plays nicely with other concurrently running code. We’ve decided to sprinkle in a Task.yield() every 1,000 numbers we check, allowing other tasks in the cooperative thread pool to run, and making sure that we don’t block a thread for too long.
— 44:01
It is easy enough to write a test for this. We can start the model off with a really big number, like say 10,000, and then increment, decrement and ask for the 10,000th prime: func testCount() async { let model = FeatureModel( clock: ImmediateClock(), count: 10_000 ) model.incrementButtonTapped() XCTAssertEqual(model.count, 10_001) model.decrementButtonTapped() XCTAssertEqual(model.count, 10_000) let prime = await model.nthPrime() XCTAssertEqual(model.message, "10000th prime is 104729") }
— 45:14
This test passes, and we are getting some really good test coverage on our feature even though it involves some pretty complex async work.
— 45:30
But the weird part is that we are providing an ImmediateClock to the model even though we don’t expect any time-based asynchrony to happen. Certainly asynchrony is happening since we have the Task.yield , but no time-based asynchrony is happening. Heck, we could even use a continuous clock or suspending clock since the clock is never touched.
— 46:00
If we had the concept of an “unimplemented” clock that causes an XCTest failure anytime someone tries to use its sleep method, we would have definitive proof that this test does no time-based asynchrony, making our test stronger than it is now. And in the future if we start doing time-based asynchrony, we will get instant feedback from the test suite letting us know that there is extra behavior happening in our feature that needs to be accounted for in some way.
— 46:30
So, let’s implement this “unimplemented” clock. We can basically copy-and-paste the ImmediateClock we have already defined.
— 46:43
And the one change we want to make is to invoke XCTFail if the sleep method is ever invoked: public func sleep( until deadline: Instant, tolerance: Instant.Duration? = nil ) async throws { XCTFail("Clock.sleep is unimplemented") } Cannot find ‘XCTFail’ in scope
— 46:53
Unfortunately we can’t do that because XCTFail is not available. We can’t even import XCTest to get access to it: import XCTest No such module ‘XCTest’
— 47:03
There are tricks you can do to force XCTest to link with the application, but as soon as you do that you cannot run the application in a simulator or device.
— 47:12
This is what motivated us to open source our library, XCTestDynamicOverlay , which allows you to use XCTFail without worrying if the symbol is actually available. In tests it will actually invoke the real XCTFail under the hood, and in non-tests it is just a no-op. This makes it possible to ship testing tools as Swift packages, which otherwise would be impossible.
— 47:37
So, let’s import XCTestDynamicOverlay: import XCTestDynamicOverlay
— 47:44
And use Xcode’s fancy feature to add the library to our project.
— 47:57
And now everything magically compiles.
— 48:02
Let’s take it a step further. If we ask the unimplemented clock for its current instant, we should also fail: public var now: Instant { XCTFail("Clock.now is unimplemented") return Instant() }
— 48:29
And we can now use the UnimplementedClock in our new test instead of using an immediate clock: func testCount() async { let model = FeatureModel( clock: UnimplementedClock(), count: 10_000 ) model.incrementButtonTapped() XCTAssertEqual(model.count, 10_001) model.decrementButtonTapped() XCTAssertEqual(model.count, 10_000) let prime = await model.nthPrimeButtonTapped() XCTAssertEqual(prime, 104_729) }
— 48:37
This still passes, which proves definitively that the clock is never touched in this one execution path, for if it was it would have caused a test failure.
— 48:52
To see this, suppose that we wanted to do something a little more interesting when the nth prime button is tapped. For example, what if we wanted to measure how long the computation takes and feed that information back to our analytics system, segmented by device. func nthPrime() async throws -> Int { let duration = await self.clock.measure { var primeCount = 0 var prime = 2 while primeCount < self.count { defer { prime += 1 } if isPrime(prime) { primeCount += 1 } else if prime.isMultiple(of: 1_000) { await Task.yield() } } self.message = "\(self.count)th prime is \(prime - 1)" } // TODO: track duration with backend _ = duration }
— 49:46
If we run our test, however, we get a failure: func testCount() async throws { let model = FeatureModel( clock: UnimplementedClock(), count: 10_000 ) model.incrementButtonTapped() XCTAssertEqual(model.count, 10_001) model.decrementButtonTapped() XCTAssertEqual(model.count, 10_000) await model.nthPrimeButtonTapped() XCTAssertEqual(model.message, "10000th prime is 104729") } testCount(): Clock.now is unimplemented
— 50:00
It is not enough that all of our assertions are correct. We are now using a dependency in a way that has not be accounted for in this test. We are now forced to provide an implemented clock to prove that we know this feature is going to be doing time-based asynchronous work. We could of course use a continuous clock, and then we’d want to write some assertions that the analytics were being tracked: let model = FeatureModel( clock: ContinuousClock(), count: 10_000 ) … // TODO: assert against analytics event
— 50:34
We wouldn’t assert against the amount of time the tracking took, since it will not be the same amount of time each time, but we would want to assert that an event was tracked in the first place. Next time: Test clocks
— 51:18
So, this is pretty amazing. After diving deep into the Clock protocol so that we could understand what core concept it represents, and seeing how it differs from Combine schedulers, we showed how to make use of clocks in our features. There were a number of false starts to do that, first needing to wrap our minds around clock existentials and primary associated types and then coming to grips with some missing APIs in the standard library, but once we got out of the weeds we had the ability to swap clocks in and out of our feature code. We could use a continuous clock in the feature when running on a device, but sub out for a more controllable clock in tests and SwiftUI previews.
— 51:53
That led us to the creation of the “immediate” clock and the “unimplemented” clock. Both are powerful, and allow you to make your tests stronger and make it so that you don’t have to literally wait for time to pass in order to see how your feature behaves.
— 52:06
But there’s one more kind of clock that we want to have for our tests, and it’s even more powerful than any of the other clocks we have discussed. While the immediate clock allows us to squash all of time into a single instant, that isn’t always what we want. Sometimes we want to actually control the flow of time in our feature so that we can see how multiple time-based asynchronous tasks interleave their execution, or so that we can wiggle ourselves in between time-based tasks to see what is happening between events.
— 52:38
This is something we explored quite a bit in our series of episodes on Combine schedulers . When writing highly reactive code that needs to leverage time-based operators, such as delays, debounces and throttles, it becomes very important to have a scheduler for which you can explicitly control the flow of time.
— 52:54
So, let’s see why exactly we want this tool, and what it takes to implement it…next time! References SE-0329: Clock, Instant, and Duration Philippe Hausler • Sep 29, 2021 The proposal that introduced the Clock protocol to the Swift standard library. https://github.com/apple/swift-evolution/blob/main/proposals/0329-clock-instant-duration.md Reply to Pitch: 'Primary Associated Types in the Standard Library' Brandon Williams & Stephen Celis • Apr 6, 2022 Originally there was some question as to whether or not the Clock protocol should have a primary associated type. We took to the forums to help motivate it. https://forums.swift.org/t/pitch-primary-associated-types-in-the-standard-library/56426/30 SE-0374: Add sleep(for:) to Clock Brandon Williams & Stephen Celis • Sep 19, 2022 A Swift Evolution proposal from yours truly that introduced a sleep(for:) method to Clock , making it possible for clock existentials to sleep. https://github.com/apple/swift-evolution/blob/main/proposals/0374-clock-sleep-for.md Downloads Sample code 0209-clocks-pt1 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .