EP 112 · Designing Dependencies · Aug 10, 2020 ·Members

Video #112: Designing Dependencies: Reachability

smart_display

Loading stream…

Video #112: Designing Dependencies: Reachability

Episode: Video #112 Date: Aug 10, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep112-designing-dependencies-reachability

Episode thumbnail

Description

It’s straightforward to design the dependency for interacting with an API client, but sadly most dependencies we work with are not so simple. So let’s consider a far more complicated dependency. One that is long living, and involves extra types that we can’t even construct ourselves.

Video

Cloudflare Stream video ID: 60c2677eb479fc2df63fd735a9412927 Local file: video_112_designing-dependencies-reachability.mp4 *(download with --video 112)*

References

Transcript

0:05

And now everything builds, albeit a little slowly because our live implementation is purposely taking a long time to build. The WeatherFeature framework builds instantly, and this means we can rapidly iterate on it, even on a clean build.

0:19

The SPM linker gotchas were definitely a pain, but we were luckily able to work through them. In the future Xcode will hopefully make everything a bit more streamlined here and take care of these kinds of linker issues automatically.

0:51

But let’s stop to appreciate what we’ve done here because this is really powerful. We are getting a ton of benefits all at once just by properly designing this dependency.

1:00

First we are able to load up our screen in many different states and edge cases, such as when the API returns no data, or some data, or a failure, and even when the request just takes a long time to finish.

1:13

And the mere fact that we can control the dependency in this way means this feature will be easy to test. We haven’t done that yet, but we will do that soon.

1:21

And last, by pushing ourselves to properly control and separate the dependency from our application code we opened ourselves up to some nice compile time optimizations. We can now build features in isolation without building their dependencies. The only time we need to actually build a live dependency, whether it be Alamofire, Starscream, or some huge shared C++ library, is when building the full app that will actually run in the simulator or on your device.

2:05

So, there is a lot of power in extracting a dependency from application code and separating its interface from its implementation. I think a lot of people would assume that the only way to do this is to use protocols, but that’s just not true. You can do it just as easily using plain, concrete data types, and there’s even a few perks to doing it in this style. Introducing a long-living dependency

2:40

Although this is very cool, let’s not stop here. Let’s add a new feature to the app that makes use of an even more complicated dependency in order to see how it can be designed to be as powerful as the weather client.

2:53

We are going to implement the functionality that makes our screen listen for internet connectivity status, so that when the device loses its internet connection we will clear the weather results and show a small error prompt at the bottom of the screen. And then, when we regain internet access we will fetch the freshest weather results from the network.

3:13

We’ll jump to a new playground so that we have a scratch pad to mess around with. The Apple framework we can use for listening to connectivity changes is called Network , so we can import it: import Network

3:23

Then we construct an object called an NWPathMonitor : let pathMonitor = NWPathMonitor()

3:29

This object can be configured with a few options, like if you wanted to only listen for certain types of connectivity, such as wifi, cellular or ethernet.

3:44

In order to be notified of updates when connectivity changes, we need to use another couple APIs on the monitor. First we must assign a callback closure to a handler, which is invoked whenever the network path is updated. pathMonitor.pathUpdateHandler = { path in }

4:10

NWPath has a bunch of properties on it that you may find useful, but for right now we only care about the connection status. pathMonitor.pathUpdateHandler = { path in print(path.status) }

4:22

And finally we start the monitor by specifying what queue we want the path updates to be delivered on: pathMonitor.start(queue: .main)

4:35

And with that we can run our playground and immediately see that we are connected to the internet. If I then switch off my wifi we’ll see that we get notified of a status change to “unsatisfied.” And then if I switch my wifi back on we get… well we didn’t get “satisfied.” We get another “unsatisfied.” satisfied unsatisfied unsatisfied

5:07

Unfortunately, the NWPathMonitor API doesn’t work super well in iOS playgrounds or even iOS simulators. It works fine on devices, but playgrounds and simulators don’t behave as expected. So, let’s quickly switch this playground over to macOS and try that again.

5:38

And now we see the correct behavior. satisfied unsatisfied satisfied

6:18

So now that we’re familiar with this API, let’s integrate it with our WeatherFeature . Currently the view model is responsible for tracking connectivity, and it merely hard-codes a boolean on initialization that never changes. public class AppViewModel: ObservableObject { … public init( isConnected: Bool = true, weatherClient: WeatherClient ) { self.isConnected = isConnected

6:37

What we want to do is update this value whenever we get reachability updates. So let’s import the Network framework. import Network

6:47

And then in the initializer, instead of passing isConnected along, we can introduce a path monitor: public init( // isConnected: Bool = true, weatherClient: WeatherClient ) { let pathMonitor = NWPathMonitor() … }

6:57

In order to update isConnected over time we can assign an update handler that does this work before starting the monitor. // self.isConnected = isConnected pathMonitor.pathUpdateHandler = { [weak self] path in self?.isConnected = path.status == .satisfied } pathMonitor.start(queue: .main)

7:39

If the path updates and is connected, we want to refresh the weather, so we can extract this logic to a helper method that can be called from the path update handler and later on in the initializer. let weatherClient: WeatherClient … init(weatherClient: WeatherClient) { self.weatherClient = weatherClient … pathMonitor.pathUpdateHandler = { [weak self] path in guard let self = self else { return } self.isConnected = path.status == .satisfied if self.isConnected { self.refreshWeather() } } … self.refreshWeather() } … func refreshWeather() { self.refreshWeatherCancellable = self.weatherClient .weather() .sink( receiveCompletion: { _ in }, receiveValue: { [weak self] in self?.weatherResults = $0.consolidatedWeather } ) }

8:45

We can also reset the weather results whenever the network becomes unreachable. pathMonitor.pathUpdateHandler = { [weak self] path in guard let self = self else { return } self.isConnected = path.status == .satisfied if self.isConnected { self.refreshWeather() } else { self.weatherResults = [] } }

8:55

And whenever we refresh the weather. func refreshWeather() { self.weatherResults = [] … }

9:02

Everything now builds and we can see that if we turn wi-fi off, the connectivity warning renders at the bottom of the screen. If we turn wi-fi back on…well the error sticks around but that’s just because of the buggy simulator behavior we saw earlier. Controlling a long-living dependency

9:44

So, the feature seems to be technically working, but it’s far from ideal right now. We have now introduced another dependency to this view model in an uncontrolled way. This has caused us to lose the ability to simulate what this screen looks like when internet connectivity changes, which we could previously do by simply passing in a boolean when creating the view model. And so the only way to inspect those states in a SwiftUI preview or simulator is to literally turn our internet off and on, which is definitely annoying and may not be convenient to do, for example you could be downloading 9 gigabytes of the newest Xcode beta.

10:12

And even worse, the path monitor API simply does not behave as expected in iOS simulators. So even if you are willing to constantly disrupt your internet connectivity you may not actually see what it would look like if you had run it on a device.

10:27

All of these problems stem from the fact that our new dependency is not controlled. We shouldn’t be at the mercy of Apple’s APIs and an internet connection to simulate the very simple idea of our application being online or offline. We need to wrap the path monitor’s functionality in a new client type, and explicitly pass it into our view model for it to use.

10:51

So we can introduce a new struct for this dependency. struct PathMonitorClient { }

11:21

Which will simply wrap the functionality of NWPathMonitor under the hood.

11:25

Now we must add closure fields for all of the functionality we want to capture. For example, we can capture the pathUpdateHandler setter in closure that does the assignment. struct PathMonitorClient { var setPathUpdateHandler: (@escaping (NWPath) -> Void) -> Void }

12:03

It’s a little gnarly looking, but it is the shape we need here. In particular the pathUpdateHandler property on path monitors is a (NWPath) -> Void callback function, so we need to take this assigned handler as input to a closure that can do the assignment.

12:20

And we can also hold the logic to start listening for path updates on a given dispatch queue. struct PathMonitorClient { var setPathUpdateHandler: (@escaping (NWPath) -> Void) -> Void var start: (DispatchQueue) -> Void }

12:43

With this client defined we can inject it into our view model initializer alongside the weather client, and update our path monitor logic to use it. public init( pathMonitorClient: PathMonitorClient, weatherClient: WeatherClient ) { // pathMonitor.pathUpdateHander = { [weak self] path in pathMonitorClient.setPathUpdateHandler { [weak self] path in guard let self = self else { return } self.isConnected = path.status == .satisfied if self.isConnected { refreshWeather() } else { self.weatherResults = [] } } // pathMonitor.start(queue: .main) pathMonitorClient.start(.main) … }

13:31

And because the initializer is public we must make the client public as well. public struct PathMonitorClient { … }

13:54

This is enough to get everything compiling except for our preview, where we must supply a path monitor client. Rather than create one inline we will open up the PathMonitorClient type and define values as statics. We can start with the “live” client. This will be an instance of the PathMonitorClient type, but secretly under the hood its endpoints will be accessing a hidden NWPathMonitor . To do this we can create an instance of the real path monitor, capture it in the endpoint closures, and forward all the information to the underlying real instance: extension PathMonitorClient { static var live: Self { let monitor = NWPathMonitor() return Self( setPathUpdateHandler: { callback in monitor.pathUpdateHandler = callback }, start: { queue in monitor.start(queue: queue) } ) } }

16:03

We can even shorten this code by using $0 and passing method references directly: extension PathMonitorClient { static var live: Self { let monitor = NWPathMonitor() return Self( setPathUpdateHandler: { monitor.pathUpdateHandler = $0 }, start: monitor.start(queue:) ) } } Controlling a model you can’t construct

16:28

We could now supply the live client to our preview and everything would work exactly as before, in particular if we disconnected from the internet we’d see the connectivity error appear, and if we reconnected we would experience the simulator behavior where it still thinks we’re offline. This is why being able to control dependencies like this is important, because we can create some mocks that can be used more reliably in the simulator. We can start by mocking out a client that has a “satisfied” connection extension PathMonitorClient { static let satisfied = PathMonitorClient( setPathUpdateHandler: <#(@escaping (NWPath) -> Void) -> Void#>, start: <#(DispatchQueue) -> Void#> ) }

17:38

In order to implement the setPathUpdateHandler we need to provide a closure that takes a closure as input, and that closure receives an NWPath : setPathUpdateHandler: { callback in callback(/*satisfied*/) }

17:57

We’d like to invoke the callback immediately with a satisfied path, however there doesn’t seem to be a public initializer on NWPath : NWPath.init

18:08

This is a bummer and sadly will be the common reality of working with dependencies you don’t own. In order to work around this we need to recreate a type that mimics the NWPath interface, and that way we are free to construct them in any configuration we want.

18:36

We can call it NetworkPath . struct NetworkPath { }

18:44

Note that we call this type NetworkPath instead of just Path in order to prevent conflicts with SwiftUI’s Path type. Usually we would just drop the

NW 19:02

Next we add all the fields to this type that we are interested in accessing. Right now the only thing we care about is the status value, and thankfully its type is NWPath.Status , which is an enum, and so they can be freely constructed by us: struct NetworkPath { var status: NWPath.Status }

NW 19:29

And we can leave it here for now, and add more fields from NWPath in the future as we need them.

NW 19:39

We will also provide a convenience initializer to our NetworkPath type so that it can be easily created from an NWPath value: extension NetworkPath { init(rawValue: NWPath) { self.status = rawValue.status } }

NW 20:05

And now we can use this our path type in the client interface instead. struct PathMonitorClient { var setPathUpdateHandler: (@escaping (NetworkPath) -> Void) -> Void var start: () -> Void }

NW 20:32

This extra boilerplate of recreating data types can be a little annoying, and can grow with each part of a dependency that you want to control, but luckily it is quite mechanical to write. And in this case it has given us the ability to create mock clients to easily exercise and test all of the functionality of a path monitor. And the more Apple embraces simple value types that are equatable and constructible in its frameworks, the easier it will be for us to control them.

NW 21:07

In the live client we must now convert the NWPath to a NetworkPath . extension PathMonitorClient { static var live: Self { let pathMonitor = NWPathMonitor() return Self( setPathUpdateHandler: { callback in pathMonitor.pathUpdateHandler = { path in callback(NetworkPath(rawValue: path)) } }, start: pathMonitor.start ) } }

NW 21:58

And now we can finish defining our mock “satisfied” client using the NetworkPath type. We can stub the start endpoint to be just an empty closure for now, but soon it will be handy to do something custom in there: extension PathMonitorClient { static let satisfied = PathMonitorClient( setPathUpdateHandler: { callback in callback(NetworkPath(status: .satisfied)) }, start: { _ in } ) }

NW 22:43

And we can make a similar mock for the “unsatisfied” case. extension PathMonitorClient { static let unsatisfied = PathMonitorClient( setPathUpdateHandler: { callback in callback(NetworkPath(status: .unsatisfied)) }, start: { _ in } ) } And now we can start using these mocks in our previews. For example a “satisfied” mock: struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView( viewModel: AppViewModel( pathMonitorClient: .satisfied, … ) ) } }

NW 23:08

When we run the preview we’ll see everything behaves just fine, but if we swap the client out for an “unsatisfied” mock. // pathMonitorClient: .satisfied, pathMonitorClient: .unsatisfied,

NW 23:31

We see the failure displayed, which is great, although we’re also seeing some weather results, which is not. It looks like we’ve uncovered a small bug: we call out to refresh the weather when we initialize the view model, but really we should just wait for path monitor to let us know we’re in a connected state. init(…) { … // self.refreshWeather() } Mocking a long-living dependency

NW 24:58

But how can we be sure that our application is properly responding to connectivity changes? Let’s define another mock that simulates a flakey connection that keeps going from satisfied to unsatisfied and back again. extension PathMonitor { static let flakey = Self( setPathUpdateHandler: { callback in var status = NWPath.Status.satisfied Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { _ in callback(NetworkPath(status: status)) status = status == .satisfied ? .unsatisfied : .satisfied } }, start: { } ) }

NW 26:58

And if we use this mock in our preview. // pathMonitorClient: .satisfied, // pathMonitorClient: .unsatisfied, pathMonitorClient: .flakey,

NW 27:10

We’ll see the connectivity flip every couple seconds, so we are indeed reacting to the dependency’s stream of updates!

NW 27:29

There’s one thing about this dependency that still isn’t quite right. When we call start on the network path it will keep it alive forever, and it will evaluate the path update handler no matter what, even if we’ve navigated away to a screen that doesn’t need to know the connectivity status. In particular the path monitor has a cancel method that, when called, will stop sending path updates to its handler, and we would hope to call to this API when the view model deinits, but this endpoint doesn’t exist on the client just yet. deinit { pathMonitorClient.cancel() }

NW 28:04

Also, in order to get access to the path monitor client we must update the view model to hold onto it outside of the initializer. let pathMonitorClient: PathMonitorClient init(…) { … self.pathMonitorClient = pathMonitorClient … } deinit { self.pathMonitorClient.cancel() }

NW 28:15

And then we need a new cancel endpoint on our client interface. struct PathMonitorClient { var setPathUpdateHandler: (@escaping (NetworkPath) -> Void) -> Void var start: () -> Void var cancel: () -> Void }

NW 28:26

And update each instance accordingly.

NW 28:30

The live implementation can forward its call to the underlying NWPathMonitor . static var live: Self { let pathMonitor = NWPathMonitor() return Self( cancel: { pathMonitor.cancel() }, setPathUpdateHandler: { callback in pathMonitor.pathUpdateHandler = { path in callback(NetworkPath(rawValue: path)) } }, start: pathMonitor.start ) }

NW 28:35

And our mocks can simply do nothing. extension PathMonitorClient { static let satisfied = Self( cancel: { } … ) static let unsatisfied = Self( cancel: { } … } static let flakey = Self( cancel: { } … ) }

NW 29:06

So we have now fully captured the long-living dependency’s life cycle: our view model can hand it a path update handler, start it, and cancel it upon deinitialization.

NW 29:20

We have now controlled NWPathMonitor with another client type, which gives us an opportunity to further modularize by extracting PathMonitorClient to its own package. We can repeat the exact same steps we took before when we created the WeatherClient package.

NW 30:18

But rather than go through all of them here live, let’s skip ahead with a lil movie magic.

NW 30:31

✨🎩✨ And here we are! We have a new PathMonitorClient package, with PathMonitorClient and PathMonitorClientLive products, ready to go! And we’ve already imported them into our WeatherFeature framework and app target.

NW 32:05

We’ve now controlled and isolated our second dependency, and though it was a little more complicated than the first, the process was still quite mechanical and easy to do:

NW 32:15

We defined another “client” struct to wrap the functionality of NWPathMonitor .

NW 32:23

Then we defined closure properties for each of its endpoints we care about.

NW 32:30

Then we defined a “live” instance that forwards its endpoints to a hidden NWPathMonitor under the hood, and we defined a few “mock” instances that simulate various states of connectivity.

NW 33:05

We also hit a bump in which we needed to create our own type to represent an NWPath , because this type is not constructible, but we believe the benefits clearly outweighed the boilerplate, as we were instantly able to simulate “satisfied” and “unsatisfied” paths, and even a network that’s on the fritz, which was important to do, seeing as the “live” client doesn’t even behave correctly in playgrounds, previews, or even the simulator. Improving a dependency’s interface

NW 34:22

One of the benefits of wrapping a dependency in a type we own is that it gives us an opportunity to take a harder look at its API and maybe even make some improvements to it.

NW 34:31

Currently, we capture this dependency as a struct with three fields. public struct PathMonitorClient { public var cancel: () -> Void public var setPathUpdateHandler: (@escaping (NetworkPath) -> Void) -> Void public var start: () -> Void … }

NW 34:38

The way NWPathMonitor handles subscribing to changes is with a block-based API, which when compared to delegate-based APIs is relatively modern. But we can do even better. Apple now gives us first party support for reactive programming, via the Combine framework, and publisher-based APIs can be even nicer to use than the callback-based APIs. For instance, URLSession now has that nice Combine API for performing a data task rather than using the callback version.

NW 35:03

Unfortunately Apple hasn’t Combine-ified all of its APIs just yet, but that doesn’t mean we should not feel empowered to fill these gaps ourselves in the meantime.

NW 35:12

In this case we have a path update handler, which can receive NetworkPath s over time, and in Combine this can be modeled in a publisher. var networkPathPublisher: AnyPublisher<NetworkPath, Never> There is no way for path updates to “fail,” so the failure type can be Never .

NW 35:54

We can then bundle the functionality of start and cancel inside the publisher so that when someone first subscribes to it, start is called under the hood, and when the publisher completes or is canceled, this call is automatically forwarded on to the underlying path monitor. // var start: (DispatchQueue) -> Void // var cancel: () -> Void

NW 36:30

We can now update the client’s initializer to take this single publisher instead of 3 separate endpoints. public init( pathUpdatePublisher: AnyPublisher<NetworkPath, Never> ) { self.pathUpdatePublisher = pathUpdatePublisher }

NW 36:44

These changes have broken our mocks, but we can fix things easily enough by deleting the empty start and stop fields. public static let satisfied = Self( pathUpdatePublisher: Just(NetworkPath(status: .satisfied)) .eraseToAnyPublisher() ) public static let unsatisfied = Self( pathUpdatePublisher: Just(NetworkPath(status: .unsatisfied)) .eraseToAnyPublisher() )

NW 37:26

And we can even utilize some fancy Combine machinery for the flakey mock: public static var flakey: Self { Self( pathUpdatePublisher: Timer .publish(every: 2, on: .main, in: .default) .autoconnect() .scan(.satisfied) { status, _ in status == .satisfied ? .unsatisfied : .satisfied } .map { NetworkPath(status: $0) } .eraseToAnyPublisher() ) }

NW 38:29

What changes do we need to make to the “live” client? We now need to construct a path update publisher. public static var live: Self { let monitor = NWPathMonitor() return Self( // cancel: pathMonitor.cancel, // setPathUpdateHandler: { callback in // pathMonitor.pathUpdateHandler = { path in // callback(NetworkPath(path)) // } // }, // start: { pathMonitor.start(queue: $0) } pathUpdatePublisher: <#AnyPublisher<NetworkPath, Never>#> ) } Which we can do by creating a “subject,” which is a lightweight way of constructing a publisher that we can send values to: let subject = PassthroughSubject<NWPath, Never>()

NW 39:13

This subject will form the basis of what we will return from this function, but we need to erase it first. let subject = PassthroughSubject<NWPath, Never>() … pathUpdatePublisher: subject.eraseToAnyPublisher() Cannot convert return expression of type ‘AnyPublisher<NWPath, Never>’ to return type ‘AnyPublisher<NetworkPath, Never>’

NW 39:23

We can use map to convert it to our NetworkPath type. networkPathPublisher: subject .map { path in NetworkPath(rawValue: path) } .eraseToAnyPublisher()

NW 39:41

And we can even pass along the initializer function directly to clean things up. .map(NetworkPath.init(rawValue:))

NW 39:47

This is building but in order to receive path updates we need to configure the monitor’s path update handler to call the subject’s send method. monitor.pathUpdateHandler = { path in subject.send(path) }

NW 40:10

This line, too, can be cleaned up a lil. We can assign subject.send directly because it has the same (NWPath) -> Void shape as the handler. monitor.pathUpdateHandler = subject.send

NW 40:22

We must remember to start the path monitor on the given queue. While we could call directly to the main queue here, it’d be nice to offer the API consumer to choose which queue results are delivered on, so we can convert the live dependency to be a function that can be handed a queue so that we can pass it along to the path monitor. public static func live(queue: DispatchQueue) -> Self { … monitor.start(queue: queue) … }

NW 40:51

And this should basically be working, but we also want to account for cancellation. When the path update publisher is cancelled through its cancellable we would like to also cancel the path monitor. Combine comes with an operator that lets us hook into a number of publisher events, including cancellation, called handleEvents . We will use this operator to call cancel on the path monitor when the publisher receives a cancel event. pathUpdatePublisher: { queue in let subject = PassthroughSubject<NWPath, Never>() pathMonitor.pathUpdateHandler = subject.send pathMonitor.start(queue: queue) return subject .handleEvents(receiveCancel: { monitor.cancel() }) .map(NetworkPath.init(rawValue:)) .eraseToAnyPublisher() }

NW 41:20

We can even pass monitor.cancel directly, since receiveCancel takes a () -> Void closure: .handleEvents(receiveCancel: monitor.cancel)

NW 41:27

Things are still not quite right here because the path monitor will start immediately after pathUpdatePublisher is invoked, and ideally we want to wait till the publisher is subscribed to. We can do this by tapping into the receiveSubscription endpoint of the handleEvents method: .handleEvents( receiveSubscription: { _ in monitor.start(queue: queue) }, receiveCancel: monitor.cancel ) Using the streamlined dependency

NW 42:01

Okay, so we were able to whittle our live client down from 3 endpoints to 1 unified one using Combine, though the inner workings of the dependency definitely got more complicated in the process. Instead of simply wrapping each endpoint in a closure, we spent some time building up a custom publisher that coordinates these endpoints under the hood. In doing so, though, we should now be able to streamline the logic in the view model, which previously was responsible for hitting each of these 3 endpoints individually and at the right time.

NW 42:27

The first error is in the initializer, where we were setting the path update handler: self.pathMonitorClient.setPathUpdateHandler { [weak self] path in … } Value of type ‘PathMonitorClient’ has no member ‘setPathUpdateHandler’ Instead, we should use the pathUpdatePublisher and sink on it: //self.pathMonitorClient.setPathUpdateHandler { [weak self] path in self.pathMonitorClient.pathUpdatePublisher.sink { [weak self] path in guard let self = self else { return } self.isConnected = path.status == .satisfied if self.isConnected { self.refreshWeather() } else { self.weatherResults = [] } } Result of call to ‘sink(receiveValue:)’ is unused

NW 43:01

Because we’re now using a Combine publisher we need to hold onto the cancellable it returns, so we will add another property to our view model to hold it. class AppViewModel: ObservableObject { … private var pathUpdateCancellable: AnyCancellable? … public init( pathMonitorClient: PathMonitorClient, weatherClient: WeatherClient ) { … self.pathUpdateCancellable = self.pathMonitorClient .pathUpdatePublisher .sink { [weak self] path in … } … } … }

NW 43:20

The next error is where we call start on the monitor. self.pathMonitorClient.start() Value of type ‘PathMonitorClient’ has no member ‘start’ This is now done for us automatically when we call sink on the publisher, so we can get rid of it, which is pretty nice! It’s easy to forget to call start , like calling resume on a URLSessionDataTask , so it’s nice that Combine publishers can clean this up. // self.pathMonitorClient.start()

NW 43:25

We also have an error where we call cancel on deinit . deinit { self.pathMonitorClient.cancel() } Value of type ‘PathMonitorClient’ has no member ‘cancel’ This too, is handled for us automatically when the cancellable is deinitialized, so we can delete this code as well. // deinit { // self.pathMonitorClient.cancel() // }

NW 43:35

So this is pretty nice! We were able to refactor our view model to interact with this dependency in a single sink , and we have pushed all of the complexity of starting and stopping the underlying path monitor to the dependency itself.

NW 43:44

But even better, Combine gives us a ton of bells and whistles that we can now employ. For example, if we only want to be notified when isConnected changes, we can map to that state and then use the removeDuplicates operator. pathUpdateCancellable = self.pathMonitorClient.pathUpdatePublisher .map { $0.status == .satisfied } .removeDuplicates() .sink { [weak self] isConnected in guard let self = self else { return } self.isConnected = isConnected if self.isConnected { self.refreshWeather() } else { self.weatherResults = [] } }

NW 44:50

And now we are able to minimize the number of refreshes for path updates where the status hasn’t changed.

NW 44:46

Fully controlling the dependency in a type we own allowed us to reimagine and modernize its API using Combine, a framework that didn’t even exist when NWPathMonitor was first introduced. We were able to clean up a lot of messy logic in our view model around starting and canceling the monitor by baking it into the publisher’s lifecycle. And what we’re seeing here is that this style of dependency is even more flexible in allowing us to evolve APIs. Had we merely slapped a protocol onto NWPathMonitor to control it, we may have not been so bold as to rethink its design. Next time: controlling Core Location

NW 45:28

We now have a weather app that is perfectly useful for the inhabitants of New York City. 😂 It’d be nice for it to be useful to folks in any location, though. So let’s introduce yet another dependency, Core Location, so that we can fetch a user’s local weather, instead. Core Location is our most complex dependency yet. It employs manager objects and delegates to handle a complex state machine of events out in the real world.

NW 46:14

But before we integrate with this framework we need to add more functionality to our weather API client so that we can load data from different locations…next time! References Protocol-Oriented Programming in Swift Apple • Jun 16, 2015 Apple’s eponymous WWDC talk on protocol-oriented programming: At the heart of Swift’s design are two incredibly powerful ideas protocol-oriented programming and first class value semantics. Each of these concepts benefit predictability, performance, and productivity, but together they can change the way we think about programming. Find out how you can apply these ideas to improve the code you write. https://developer.apple.com/videos/play/wwdc2015/408/ Collection: Protocol Witnesses Brandon Williams & Stephen Celis Note Protocols are great! We love them, you probably love them, and Apple certainly loves them! However, they aren’t without their drawbacks. There are many times that using protocols can become cumbersome, such as when using associated types, and there are some things that are just impossible to do using protocols. We will explore some alternatives to protocols that allow us to solve some of these problems, and open up whole new worlds of composability that were previously impossible to see. https://www.pointfree.co/collections/protocol-witnesses Downloads Sample code 0112-designing-dependencies-pt3 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 .