Video #205: Reducer Protocol: Dependencies, Part 1
Episode: Video #205 Date: Sep 19, 2022 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep205-reducer-protocol-dependencies-part-1

Description
We begin to flesh out a new story for dependencies in the Composable Architecture, taking inspiration from SwiftUI. We will examine SwiftUI’s environment and build a faithful reproduction that provides many of the same great benefits.
Video
Cloudflare Stream video ID: 9468078fb3e722e3d37f3ee01401b55e Local file: video_205_reducer-protocol-dependencies-part-1.mp4 *(download with --video 205)*
Transcript
— 0:05
We have now made huge improvements to the ergonomics of defining reducers, which make it simpler and more natural to build features with the Composable Architecture. We now implement reducers as types that conform to the ReducerProtocol , and we can do so in one of two ways: by implementing a reduce method that mutates state whenever an action comes into the system, or by implementing a body computed property that expresses how to compose a bunch of reducers together.
— 0:29
Most importantly, everything we have added to the library is still 100% backwards compatible. Every existing Composable Architecture application will still compile and run exactly as it did before these changes.
— 0:39
Further, even though we are using some advanced Swift 5.7 features to make reducer builders and bodies as ergonomic as possible, we can approximate these tools for those who need to stay on Swift 5.6 for a bit longer. This means if you can’t immediately upgrade your project to Xcode 14, you can still write reducers in this style, with just a few small changes. We aren’t going to cover those details right now, but just know that it will be available in the final library release.
— 1:07
But there are more benefits to be had from this new style of defining reducers. We have already completely removed the concept of “environment” from reducers, and instead just hold onto dependencies directly in the conforming type itself, but now we can start to explore more ways to simplify dependency management. What if we could adopt a style similar to SwiftUI’s environment values, where instead of explicitly passing values throughout a view hierarchy, you can have them globally and implicitly available, and then any view can grab ahold of the value whenever they want.
— 1:38
This comes with a ton of benefits. First of all parent views do not need to hold onto dependencies it doesn’t need just so that child views have access to them. We also eliminate the need to create public initializers when modularizing our application just so that we can pass dependencies from one module to another. And we make it easy to override just a single dependency in a child feature, which can be great for running a feature in an alternative environment. SwiftUI’s Environment
— 2:05
Let’s start by theorizing what this feature could look like by taking some inspiration from SwiftUI. We’ll take a look at the voice memos demo since that has been converted to the protocol style and it has some interesting dependencies.
— 2:21
Currently the RecordingMemo reducer has two dependencies: the AudioRecorderClient for recording audio from the device, and a mainRunLoop in order to run a timer: struct RecordingMemo: ReducerProtocol { … var audioRecorder: AudioRecorderClient var mainRunLoop: AnySchedulerOf<RunLoop> … }
— 2:28
The only way a RecordingMemo reducer can be constructed is if it is passed these two values. In fact, these are the only parameters that need to be passed to construct an instance. We can see this over in the VoiceMemos reducer, where we can pass along those dependencies since they also reside inside the VoiceMemos type: .ifLet( state: \State.recordingMemo, action: /Action.recordingMemo ) { RecordingMemo( audioRecorder: self.audioRecorder, mainRunLoop: self.mainRunLoop ) }
— 2:42
SwiftUI has the concept of “environment values” that allow you to propagate values throughout a view hierarchy, which can be handy for things that need to be ubiquitously accessible.
— 2:51
The docs spell this out: Note EnvironmentValues A collection of environment values propagated through a view hierarchy.
— 2:57
And further down in the docs we will see a whole bunch of environment values that are provided by the framework. It is also possible to define your own environment values, which can be really powerful.
— 3:13
To make use of an environment value you use the @Environment property wrapper in your view. For example, we could add the openURL environment value to our RecordingMemoView : struct RecordingMemoView: View { @Environment(\.openURL) private var openURL … }
— 3:37
Interestingly we don’t need to provide a type for openURL . The type is already baked into the key path \.openURL and so Swift will infer it for us on the property. This is great since it would be too onerous to require the user to know what type openURL is and specify it: @Environment(\.openURL) private var openURL: <#???#>
— 3:54
Also interestingly, everything still compiles even though we added a new property to the view without an initial value. This is happening because the way environment values are specified you must provide a default, and so the property wrapper can start with that default, even though you are allowed to override it you want.
— 4:15
We can even add a whole bunch of environment values to our view: @Environment(\.openURL) private var openURL @Environment(\.dismiss) var dismiss @Environment(\.redactionReasons) var redactionReasons
— 4:28
…and everything still compiles.
— 4:30
This shows that it’s possible to add new environment values to a view without breaking all the code that creates the view. The parent view can be completely oblivious to what environment values the child wants, and the child can add, remove and update environment values at will.
— 4:42
Finally, to make use of the environment value you can just access the property directly to interact with it. For example, say we had a “Help” button that should open an external web page. We can do so like this: Button("Help") { self.openURL(URL(string: "https://www.pointfree.co/")!) }
— 5:03
So, that’s how you make use of environment values, and SwiftUI provides lots of useful ones out of the box, but how can we add our own values so that they can be propagated throughout the view hierarchy?
— 5:13
The process consists of a few steps that seem weird at first, but it is designed specifically so that you can add your own values to the entire world of environment values without knowing anything about that world. This allows you to add new environment values without needing to build everyone else’s environment values, which is great for modularity and having dependencies be separate and isolated.
— 5:33
You start by defining a new type that conforms to the EnvironmentKey protocol, which has one single requirement: a static default value. public protocol EnvironmentKey { associatedtype Value static var defaultValue: Value { get } }
— 5:43
So, this new type doesn’t have any functionality itself. It just needs to statically expose some default value. Although you can use any kind of type to conform to this protocol, such as struct, class or even actor, we will go with empty enum to further signify that this type isn’t meant to have any instance-level behavior: enum MyKey: EnvironmentKey { }
— 6:03
Then we need to define the default value, which is the value the environment will hold on first launch of the application, though it can be overridden depending on how we implement the value.
— 6:07
Let’s put an integer in for testing purposes: enum MyKey: EnvironmentKey { static var defaultValue = 42 }
— 6:10
This type can even be private because no one from the outside will ever need to know about this type, and the default value should be constant, so we can force this with a let . private enum MyKey: EnvironmentKey { static let defaultValue = 42 }
— 6:19
Next we extend the EnvironmentValues type to provide a computed property for our environment value. Remember that EnvironmentValues type is the “collection” of values that is propagated throughout the view hierarchy. By extending it with our computed property we are adding our value to this “collection”: extension EnvironmentValues { var myValue: Int { } }
— 6:47
Now, what can we do in here? We have access to self , which is of type EnvironmentValues , and that type has only one real piece of functionality beyond all the extension properties added from the outside. It has a subscript that takes a type as an argument, and that type must conform to EnvironmentKey : subscript<K: EnvironmentKey>(key: K.Type) -> K.Value { get set }
— 7:06
Notice that it returns the Value of the key, which in our MyValue type is just Int .
— 7:10
So, we can reach into self via the subscript, and pluck out the integer corresponding to our MyValue type: extension EnvironmentValues { var myValue: Int { self[MyKey.self] } }
— 7:16
Now currently this is only a getter, which means it cannot be later overridden. The value will stay constant at 42 for the entire lifetime of the application.
— 7:24
That can be handy, but things get even more powerful if you provide a setter. We can add this functionality easily thanks to the fact that the subscript exposed also has a set: extension EnvironmentValues { var myValue: Int { get { self[MyKey.self] } set { self[MyKey.self] = newValue } } }
— 7:39
With those few steps completed we have now added our value to the “global storage” of environment values. Interestingly we were able to do so while still preserving the type information. On the one hand we can think of EnvironmentValues as a kind of nebulous, blob of global values, but on the other hand all the types are intact for all of those values.
— 7:58
We now have the ability to add this environment value to our view: struct RecordingMemoView: View { @Environment(\.myValue) var myValue … }
— 8:07
And we can make use of it: Text("\(self.myValue)")
— 8:12
If we run the app and start a recording, we will see the value 42 displayed at the top.
— 8:21
Even cooler, we can override this value for just a subset of the view hierarchy. For example, back in the VoiceMemosView , which is responsible for showing the RecordingMemoView , we could choose to override this value just for the RecordingMemoView : RecordingMemoView(store: store) .environment(\.myValue, 1729)
— 8:45
And if we run this and start a recording we will see the value “1,729” displayed at the top.
— 8:51
Only inside RecordingMemoView and its child views is the environment value 1729. All sibling or parent views will have the default value of 42. Theorizing a TCA Environment
— 9:02
So, it looks like SwiftUI’s environment values solves most of the problems we have encountered with dependencies in the Composable Architecture. It provides a decoupled way of adding new values to a nebulous, global “storage” of values, and does so in a type safe manner. It allows child views to specify its dependence on values that the parent view is completely oblivious too, which means parent views don’t need to be updated when child views change their values. And we have the ability to override values on just a small subset of the view hierarchy.
— 9:29
Let’s see what it takes to replicate this pattern in the Composable Architecture. We would love if we could somehow mark our reducer’s dependencies with a property wrapper, which then grabs that dependency from some global storage of dependencies and has a sensible default, and do all of that without any parent domains needing to know about the child’s dependencies.
— 9:48
Not only can we accomplish this and clean up some of the annoyances we’ve seen before, but it will also unlock all new powerful patterns that were previously completely hidden from us. We will also be able to make testing in the Composable Architecture, which in our opinion is already pretty ergonomic, much, much more ergonomic.
— 10:05
Let’s start by theorizing the syntax we’d like to use at the call site, and then see what it takes to make that syntax compile.
— 10:13
What if we could specify all the dependencies a feature needs to do its job by using @Environment -like syntax, except we will call it @Dependency : struct RecordingMemo: ReducerProtocol { … // var audioRecorder: AudioRecorderClient // var mainRunLoop: AnySchedulerOf<RunLoop> @Dependency(\.audioRecorder) private var audioRecorder @Dependency(\.mainRunLoop) private var mainRunLoop … }
— 10:38
Notice that ideally we won’t even have to specify the type of the variable. Already this is nice because it hides the gross AnySchedulerOf<RunLoop> stuff we are doing, which isn’t really necessary to understand. All we really care about is that we have a scheduler that behaves like a run loop.
— 10:58
This of course doesn’t compile, but if it did work then we wouldn’t even have to change anything inside the reduce method. We could continue accessing the dependencies by just doing self.audioRecorder or self.mainRunLoop .
— 11:12
Similarly, over in the VoiceMemo reducer we could move to this new style of dependency: struct VoiceMemo: ReducerProtocol { … @Dependency(\.audioPlayer) private var audioPlayer @Dependency(\.mainRunLoop) private var mainRunLoop … }
— 11:30
And we can do the same in the VoiceMemos reducer: struct VoiceMemos: ReducerProtocol { … @Dependency(\.audioPlayer) private var audioPlayer @Dependency(\.audioRecorder) private var audioRecorder @Dependency(\.mainRunLoop) private var mainRunLoop @Dependency(\.openSettings) private var openSettings @Dependency(\.temporaryDirectory) private var temporaryDirectory @Dependency(\.uuid) var uuid … }
— 11:51
With that done, the real magic starts to happen. We no longer have to pass dependencies to child features from the parent: var body: some ReducerProtocolOf<Self> { Reduce { state, action in … } .ifLet( state: \State.recordingMemo, action: /Action.recordingMemo ) { RecordingMemo( // audioRecorder: self.audioRecorder, // mainRunLoop: self.mainRunLoop ) } .forEach( state: \State.voiceMemos, action: /Action.voiceMemo(id:action:) ) { VoiceMemo( // audioPlayer: self.audioPlayer, // mainRunLoop: self.mainRunLoop ) } }
— 12:16
The dependencies will be implicitly passed automatically.
— 12:21
And since we are no longer passing dependencies anymore, we no longer need to hold onto dependencies in the VoiceMemos reducer just so that we can pass it down to the child. In particular, the audioPlayer dependency can completely go away because the VoiceMemos feature doesn’t use it all: // @Dependency(\.audioPlayer) var audioPlayer @Dependency(\.audioRecorder) var audioRecorder @Dependency(\.mainRunLoop) var mainRunLoop @Dependency(\.openSettings) var openSettings @Dependency(\.temporaryDirectory) var temporaryDirectory @Dependency(\.uuid) var uuid
— 12:49
Even cooler, we can also chisel away at a particular dependency if we only need a small subset of its functionality. For example, the VoiceMemos feature does technically need access to the audioRecorder dependency because we use it to ask for recording permission: case .undetermined: return .task { await .recordPermissionResponse( self.audioRecorder.requestRecordPermission() ) }
— 13:16
However, the audioRecorder dependency has a lot more functionality beyond just asking for permission: struct AudioRecorderClient { var currentTime: @Sendable () async -> TimeInterval? var requestRecordPermission: @Sendable () async -> Bool var startRecording: @Sendable (URL) async throws -> Bool var stopRecording: @Sendable () async -> Void }
— 13:26
The voice memos feature doesn’t need any of that extra functionality, and we now have an easy way to make this known. We can simply chain onto the dependency key path to just pluck out what we need, in particular the requestRecordPermission endpoint: @Dependency(\.audioRecorder.requestRecordPermission) private var requestRecordPermission
— 13:49
And then the usage of that dependency in the reducer becomes simpler: case .undetermined: return .task { await .recordPermissionResponse( self.requestRecordPermission() ) }
— 13:56
And if that wasn’t cool enough, we can also choose to override certain dependencies for child features. For example, suppose we had an onboarding feature for teaching people how to use the recording feature. We could design an implementation of the AudioRecordingClient that doesn’t actually interact with AVFoundation and instead simulates it with mock data, and then pass that dependency down to the RecordingMemo feature: .ifLet( state: \State.recordingMemo, action: /Action.recordingMemo ) { RecordingMemo() .dependency(\.audioRecorder, .onboarding) }
— 14:53
We don’t actually want to override this dependency here, though, so let’s comment this out for now. // .dependency(\.audioRecorder, .onboarding) DependencyKey , DependencyValues
— 14:57
So, already this seems amazing, but it really only scratches the surface of what is possible once we have this system in place.
— 15:03
Let’s start a new file in the ComposableArchitecture module to house this API.
— 15:20
And we can start with the simplest part, which is the protocol that is analogous to the EnvironmentKey protocol from SwiftUI. It just had an associated type for the type of the value as well as a static variable requirement for describing the default value: public protocol DependencyKey { associatedtype Value static var defaultValue: Value { get } }
— 15:45
And already we can create a bunch of keys for various dependencies that are common in applications, such as a main run loop: import CombineSchedulers … private enum MainRunLoopKey: DependencyKey { static let defaultValue = AnySchedulerOf<RunLoop>.main }
— 16:17
And a main dispatch queue: import Foundation … private enum MainQueueKey: DependencyKey { static let defaultValue = AnySchedulerOf<DispatchQueue>.main }
— 16:31
As well as a dependency that can grab the current date, which is a subtle but important dependency to control: private enum DateKey: DependencyKey { static let defaultValue = { Date() } }
— 16:46
And even a
UUID 16:58
We can even define keys for opening settings, and grabbing the URL to the file system’s temporary directory. import SwiftUI … private enum OpenSettingsKey: DependencyKey { static let defaultValue = { await MainActor.run { UIApplication.shared.open( URL(string: UIApplication.openSettingsURLString)! ) } } } private enum TemporaryDirectoryKey: DependencyKey { static let defaultValue = { URL(fileURLWithPath: NSTemporaryDirectory()) } }
UUID 17:29
These dependencies can even ship as a part of the library, instantly giving everyone access to a base set of dependencies that are immediately controllable.
UUID 17:39
However, these things aren’t the actual dependencies. They are just the keys that can be used to find the dependency in the big, nebulous, blob of global dependencies, as well as the default value for when we can’t find the dependency. What we need to implement next is the analogous concept of EnvironmentValues .
UUID 17:56
We’ll call it DependencyValues , and we can get a stub into place: public struct DependencyValues { }
UUID 18:09
And just like EnvironmentValues , we need to somehow provide a subscript that takes a DependencyKey as an argument and returns a key value: public subscript<Key: DependencyKey>( key: Key.Type ) -> Key.Value { get { } set { } }
UUID 18:36
In order to implement the getter for this subscript we must have some concrete collection of values to pull from. We’ve already referred to this collection as some kind of “nebulous blob” of values, and indeed we can model it as a dictionary that has the minimal amount of type information: public struct DependencyValues { private var storage: [AnyHashable: Any] = [:] … }
UUID 19:12
This dictionary maps the key of our dependencies, which recall are the little private enums we defined that expose the default value, to the dependency itself. The dependencies type information is lost because we are storing it as an Any , but that is necessary in order for us to store any kind of value in the collection. This makes it possible for the dependency storage to know nothing about the dependencies being held, and in particular we don’t need to be able to compile all of those dependencies just to store them.
UUID 19:43
With the storage in place in might hope we could try keying into it with the Key type passed to the subscript: public subscript<Key: DependencyKey>( key: Key.Type ) -> Key.Value { get { self.storage[key] } set {} } No exact matches in call to subscript
UUID 19:52
…but this doesn’t work because meta types, which is what Key.Type is, are not Hashable . However, there is a way to get a stable hashable value out of meta types, and that is using ObjectIdentifier : get { self.storage[ObjectIdentifier(key)] } Cannot convert return expression of type ‘Any?’ to return type ’Key.Value’
UUID 20:13
That now gives us access to an Any value. We know what type of value we expect it to hold because that is determined by the DependencyKey , so we can try casting it: get { guard let dependency = self.storage[ObjectIdentifier(key)] as? Key.Value else { } return dependency }
UUID 20:30
And if we don’t find a dependency in the storage for that key, or if for some reason it is the wrong type, we can fallback to the default: get { guard let dependency = self.storage[ObjectIdentifier(key)] as? Key.Value else { return Key.defaultValue } return dependency }
UUID 20:42
And this now compiles.
UUID 20:45
This allows us to pluck out a statically typed dependency from the nebulous blob of dependencies, and it will default to the one specified by the DependencyKey if that dependency does not yet exist.
UUID 20:57
However, without implementing the real setter we don’t have any way of ever actually putting in a non-default dependency into the storage. We can do this by assigning through the ObjectIdentifier of the key: public subscript<Key: DependencyKey>( key: Key.Type ) -> Key.Value { get { … } set { self.storage[ObjectIdentifier(key)] = newValue } }
UUID 21:16
This completes the implementation of DependencyValues , and it now provides all the functionality we need for adding dependencies to the nebulous blob of storage. And we can do so in exactly the same way that values are added to EnvironmentValues .
UUID 21:30
For example, to stick the main run loop into the storage of DependencyValues , we can simply do: extension DependencyValues { public var mainRunLoop: AnySchedulerOf<RunLoop> { get { self[MainRunLoopKey.self] } set { self[MainRunLoopKey.self] = newValue } } }
UUID 22:03
And similarly for all of the dependency keys we defined before: extension DependencyValues { … public var mainQueue: AnySchedulerOf<DispatchQueue> { get { self[MainQueueKey.self] } set { self[MainQueueKey.self] = newValue } } public var date: () -> Date { get { self[DateKey.self] } set { self[DateKey.self] = newValue } } public var uuid: () -> UUID { get { self[UUIDKey.self] } set { self[UUIDKey.self] = newValue } } public var openSettings: () async -> Void { get { self[OpenSettingsKey.self] } set { self[OpenSettingsKey.self] = newValue } } public var temporaryDirectory: () -> URL { get { self[TemporaryDirectoryKey.self] } set { self[TemporaryDirectoryKey.self] = newValue } } }
UUID 22:20
So, there is a little bit of boilerplate in registering these dependencies with the library, but it’s really no different than what we do in SwiftUI. @Dependency
UUID 22:28
The real magic comes with implementing the property wrapper that gives us access to these values.
UUID 22:35
Recall that at the call site we want to use it like this: @Dependency(\.mainRunLoop) private var mainRunLoop
UUID 22:42
And that key path goes from the nebulous blob of DependencyValues down to a concrete, statically known dependency inside the storage. So, it seems our property wrapper will need to be generic over the type of value we expect to get out of the DependencyValues as well as hold onto that key path: @propertyWrapper public struct Dependency<Value> { let keyPath: KeyPath<DependencyValues, Value> public init( _ keyPath: KeyPath<DependencyValues, Value> ) { self.keyPath = keyPath } } Property wrapper type ‘Dependency’ does not contain a non-static property named ‘wrappedValue’
UUID 23:27
In order for this to be a valid property wrapper we must expose property called wrappedValue , which is the real thing you want exposed to the reducer even though secretly it is wrapped up in this additional type: public var wrappedValue: Value { <#???#> }
UUID 23:49
But what do we return from here? We don’t have access to a concrete DependencyValues instance so that we can search inside its storage property. In fact, we have yet to even construct a DependencyValues instance. Anywhere. Period.
UUID 24:02
Well, this brings us to another descriptive term we’ve used in reference to EnvironmentValues . We previously referred to it as a “nebulous blob” of values, and that was made clear to us since we modeled the storage as an AnyHashable -to- Any dictionary. We also referred to EnvironmentValues as a global, nebulous blob of values. This is because we can seemingly pluck a value from it at any time in the view. It’s never explicitly passed through the view hierarchy, but instead seems to just be ubiquitously available everywhere.
UUID 24:38
So, we need a global, mutable instance of DependencyValues that represents the current dependencies in the application, and we will house this value inside the DependencyValues type as a static: public struct DependencyValues { static var current = Self() }
UUID 24:53
This global, mutable value may scare you a bit, but don’t worry. We will make it safe and easy to reason about in a moment.
UUID 25:01
With this global available to us, we can implement the wrappedValue by reaching out to the global, and key-pathing into it with the key path specified when the @Dependency property wrapper was used: @propertyWrapper public struct Dependency<Value> { … public var wrappedValue: Value { DependencyValues.current[keyPath: self.keyPath] } } Under the hood this key path is invoking the getter we defined on an extension of DependencyValues , and then under that hood it calls out to the subscript we defined on DependencyValues .
UUID 25:20
Believe it or not, this is enough to get the basics of our voice memos demo application building. If we switch back over to that target we will see that we have fewer compiler errors than we had before. Swift seems to be perfectly happy with code like this: @Dependency(\.mainRunLoop) private var mainRunLoop
UUID 25:53
The only complaints are now with domain-specific dependencies, like the audio recorder and player. These are things that we wouldn’t want to ship with the core Composable Architecture library, though maybe some day they could be put into their own library.
UUID 26:06
We have to define the DependencyKey and extension on DependencyValues in order to put these dependencies in the global, nebulous storage.
UUID 26:14
For example, we can hop over to the AudioRecorderClient.swift file and add the following: private enum AudioRecorderClientKey: DependencyKey { static let defaultValue = AudioRecorderClient.live } extension DependencyValues { var audioRecorder: AudioRecorderClient { get { self[AudioRecorderClientKey.self] } set { self[AudioRecorderClientKey.self] = newValue } } }
UUID 26:49
With that small addition the @Dependency in VoiceMemos is already compiling: @Dependency(\.audioRecorder.requestRecordPermission) var requestRecordPermission
UUID 27:00
It works even though we are further reaching into only the requestRecordPermission endpoint.
UUID 27:10
We also need to add the audio player dependency, which we can do in the AudioPlayerClient.swift file: private enum AudioPlayerClientKey: DependencyKey { static let defaultValue = AudioPlayerClient.live } extension DependencyValues { var audioPlayer: AudioPlayerClient { get { self[AudioPlayerClientKey.self] } set { self[AudioPlayerClientKey.self] = newValue } } }
UUID 27:27
And with both defined we have further reduced the number of compiler errors in the project. Now the only compiler errors we have left are in places where we try to construct one of our memo reducers by specifying their dependencies, and that is no longer needed.
UUID 27:47
In the voice memos preview we can now just construct the VoiceMemos type without specifying anything whatsoever: reducer: Reducer( VoiceMemos() // ( // audioPlayer: .mock, // audioRecorder: .mock, // mainRunLoop: .main, // openSettings: {}, // temporaryDirectory: { // URL(fileURLWithPath: NSTemporaryDirectory()) // }, // uuid: { UUID() } // ) ),
UUID 27:56
And similarly in the entry point of the app: VoiceMemosView( store: Store( initialState: VoiceMemos.State(), reducer: Reducer( VoiceMemos() // ( // audioPlayer: .live, // audioRecorder: .live, // mainRunLoop: .main, // openSettings: { @MainActor in // UIApplication.shared.open( // URL( // string: UIApplication.openSettingsURLString // )! // ) // }, // temporaryDirectory: { // URL(fileURLWithPath: NSTemporaryDirectory()) // }, // uuid: { UUID() } // ) ) .debug(), environment: () ) )
UUID 28:04
Amazingly everything compiles, and the application runs exactly as it did before. We can still record and playback memos. But we have removed a massive amount of boilerplate from our code.
UUID 28:16
We no longer have to maintain an initializer for each of our reducers that lists out every dependency just so that it can be accessible from other modules. If we were to modularize this application we could simply put in an empty initializer: public init() {} …and be done with it.
UUID 28:35
Also we can now add dependencies to child features without making any changes to the parent. For example, suppose that the RecordingMemo feature wanted access to a date dependency for some reason. We can do so: struct RecordingMemo: ReducerProtocol { … @Dependency(\.date) var date … }
UUID 28:52
…and everything still compiles just fine.
UUID 29:00
This means that the parent features no longer need to hold onto the children’s dependencies just to pass them down. If the parent feature doesn’t directly need a dependency, it can omit it, just as we’ve done in the VoiceMemos feature since it doesn’t need the audio player dependency at all.
UUID 29:10
And on top of all of that it’s even possible to make dependencies “transformable” in some sense, as we can see in the voice memos feature where we pluck off just one single endpoint from a dependency: @Dependency(\.audioRecorder.requestRecordPermission) var requestRecordPermission …to make it clear that this feature doesn’t truly need the full power of the audio recorder dependency.
UUID 29:32
Now, it does seem that we have a few concurrency warnings showing in our reducers: Capture of ‘self’ with non-sendable type ‘RecordingMemo’ in a @Sendable closure Consider making struct ‘RecordingMemo’ conform to the ‘Sendable’ protocol
UUID 29:58
Before we introduced the @Dependency property wrapper to our RecordingMemo reducer, the RecordingMemo struct was actually sendable. The only fields it held onto were the audio recording and main run loop scheduler, both of which conform to Sendable , and hence the whole struct become implicitly Sendable .
UUID 30:12
However, now the struct technically holds onto Dependency values behind the scenes, and Swift’s property wrappers syntax just does a really good job of hiding that fact from us: @Dependency(\.audioRecorder) var audioRecorder // Dependency<AudioRecorderClient> var _audioRecorder
UUID 30:39
So, we need Dependency to be Sendable .
UUID 30:48
If we try conforming it to Sendable like this: public struct Dependency<Value: Sendable>: Sendable { let keyPath: KeyPath<DependencyValues, Value> … }
UUID 30:56
…we get a warning about the key path not being sendable: Stored property ‘keyPath’ of ‘Sendable’-conforming generic struct ‘Dependency’ has non-sendable type ’KeyPath<DependencyValues, Value>’
UUID 31:04
Now, many key paths are sendable. In fact, according to the Swift evolution proposal that introduced the concept of sendability to the language, all key paths except for a few special cases should be sendable.
UUID 31:16
To understand what that means, let’s first look at hashabililty and key paths. It turns out that most key paths are also hashable. struct Foo { var value = 42 } let kp1: any Hashable = \Foo.value
UUID 31:33
The fact that this compiles means that the \Foo.value key path is hashable.
UUID 31:41
Interestingly, the Foo type doesn’t need to be hashable, and in fact it isn’t. Even the value we are key-pathing to doesn’t need to be hashable: struct Foo { var value = 42 var void = () } let kp1: any Hashable = \Foo.value let kp2: any Hashable = \Foo.void
UUID 32:01
Regardless of the hashability of the root or value of the key path, it will be hashable.
UUID 32:08
The only time this breaks down is with subscript key paths, which are more nuanced because they are like “functions” that generate key paths. In order for such key paths to be hashable Swift must enforce that the data passed to the subscript is also hashable: struct Foo { var value = 42 var void = () subscript(hashable value: Int) -> Int { value } subscript(void value: ()) -> Void { value } } let kp1: any Hashable = \Foo.value let kp2: any Hashable = \Foo.void let kp3: any Hashable = \Foo.[hashable: 1] let kp4: any Hashable = \Foo.[void: ()] Subscript index of type ‘()’ in a key path must be Hashable
UUID 32:52
Swift doesn’t even allow us to construct the key path unless the data provided to the subscript is hashable.
UUID 33:13
This principle also holds for sendability of key paths, but unfortunately the diagnostics aren’t fully baked yet. Let’s cut and paste this test code over to the VoiceMemos.swift file since concurrency warnings are turned on in that target.
UUID 33:34
If we construct a key path to a normal field on the Foo type it should be sendable: let kp5: any Sendable = \Foo.value Type ‘WritableKeyPath<Foo, Int>’ does not conform to the ‘Sendable’ protocol
UUID 33:48
…but unfortunately that doesn’t seem to be the case. According to the Swift evolution proposal this should compile without a warning, and so this is a Swift compiler bug. Hopefully it will be fixed soon.
UUID 33:56
Key paths are supposed to be sendable regardless of the sendability of the root or value of the key path: let kp6: any Sendable = \Foo.void Type ‘WritableKeyPath<Foo, ()>’ does not conform to the ‘Sendable’ protocol
UUID 34:09
The only time key paths are not sendable is when constructing a subscript key path using non-sendable data, just as was the case with hashabililty.
UUID 34:30
So, this means that we just have no choice but to force sendability on the Dependency property wrapper outside the purview of the compiler: struct Dependency<Value: Sendable>: @unchecked Sendable { … }
UUID 34:41
We are pretty sure that Dependency is honest-to-goodness Sendable , it just doesn’t seem like the compiler is not fully up-to-date with the evolution proposals to prove it without warnings. Once Swift’s diagnostics are fixed we can improve this code. Next time: Overriding dependencies
UUID 35:23
There’s one last feature of our dependencies that we previously mentioned but haven’t yet implemented. And that’s the ability to override dependencies for a specific reducer. We previously alluded that this can be powerful for overriding dependencies for a child feature in order to alter the environment it operates in, such as onboarding experiences. This is definitely true, and can be incredibly powerful, but we can also see a simpler use case for this functionality in the code we’ve already written.
UUID 35:50
A moment ago when we were deleting all the initializing code of our reducers to no longer pass in dependencies, we removed the ability to pass special dependencies to the reducer for previews.
UUID 36:03
Let’s take a look at that…next time! Downloads Sample code 0205-reducer-protocol-pt5 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 .