Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
We’ve now got a moderately complex application that makes non-trivial use of 3 dependencies: a weather API client, a network path monitor, and a location manager. The weather API dependency was quite simple in that it merely fires off network requests that can either return some data or fail. The path monitor client was a bit more complex in that it bundled up the idea of starting a long-living effect that emits network paths over time as the device connection changes, as well as the ability to cancel that effect. And Core Location was the most complicated by far. It allows you to ask the user for access to their location, and then request their location information, which is a pretty complex state machine that needs to be carefully considered.
So we’ve accomplished some cool things, but I think it’s about time to ask “what’s the point?” We like to do this at the end of each series of episodes on Point-Free because it gives us the opportunity to truly justify all the work we have accomplished so far, and prove that the things we are discussing have real life benefits, and that you could make use of these techniques today.
And this is an important question to ask here because we are advocating for something that isn’t super common in the Swift community. The community already has an answer for managing and controlling dependencies, and that’s protocols. You slap a protocol in front of your dependency, make a live conformance and a mock conformance of the protocol, and then you are good to go. We showed that this protocol-oriented style comes with a bit of boilerplate, but if that’s the only difference from our approach is it really worth deviating from it?
Well, we definitely say that yes, designing your dependencies in this style has tons of benefits, beyond removing some boilerplate. And to prove this we are going to write a full test suite for our feature, which would have been much more difficult to do had we controlled things in a more typical, protocol-oriented fashion.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
Because our dependencies are simple value types we can more easily transform them. We can even define “higher-order” dependencies, or functions that take a dependency as input and transform it into a new dependency returned as output.
As an example, try implementing a method on
WeatherClient
that returns a brand new weather client with all of its endpoints artificially slowed down by a second.extension WeatherClient { func slowed() -> Self { fatalError("unimplemented") } }
Solution
You can create a new weather client by passing along all of the existing client’s endpoints with Combine’s
delay
operator attached.extension WeatherClient { func slowed() -> Self { Self( weather: { self.weather($0) .delay(for: 1, scheduler: DispatchQueue.main) .eraseToAnyPublisher() }, searchLocations: { self.searchLocations($0) .delay(for: 1, scheduler: DispatchQueue.main) .eraseToAnyPublisher() } ) } }
Implement a method on
LocationClient
that can override an existing location client to behave as if it were at a specific location.extension LocationClient { func located( atLatitude latitude: CLLocationDegrees, longitude: CLLocationDegrees ) -> Self { fatalError("unimplemented") } }
Solution
You can create a new location client by passing along each endpoint and transforming the delegate publisher to modify the
didUpdateLocations
event.extension LocationClient { func located( atLatitude latitude: CLLocationDegrees, longitude: CLLocationDegrees ) -> Self { Self( authorizationStatus: self.authorizationStatus, requestWhenInUseAuthorization: self.requestWhenInUseAuthorization, requestLocation: self.requestLocation, delegate: self.delegate .map { event -> DelegateEvent in guard case .didUpdateLocations = event else { return event } let location = CLLocation(latitude: latitude, longitude: longitude) return .didUpdateLocations([location]) } .eraseToAnyPublisher() ) } }