EP 113 · Designing Dependencies · Aug 17, 2020 ·Members

Video #113: Designing Dependencies: Core Location

smart_display

Loading stream…

Video #113: Designing Dependencies: Core Location

Episode: Video #113 Date: Aug 17, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep113-designing-dependencies-core-location

Episode thumbnail

Description

Now that we’ve tackled two dependencies of varying complexity we are ready to handle our most complicated dependency yet: Core Location. We will see what it means to control a dependency that communicates with a delegate and captures a complex state machine with many potential flows.

Video

Cloudflare Stream video ID: b9123295df1438b19a6d356ada955448 Local file: video_113_designing-dependencies-core-location.mp4 *(download with --video 113)*

References

Transcript

0:05

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.

0:47

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. Updating the weather client to search locations

1:33

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.

1:38

We are currently hard-coding a location id in our weather endpoint. .dataTaskPublisher( for: URL( string: "https://www.metaweather.com/api/location/2459115" )! )

1:49

The weather API we are hitting doesn’t allow us to fetch the weather for a particular longitude and latitude, but rather we have to first fetch a location id from the API using coordinates, and then we can use that id to load weather data.

2:07

So what we really want to do here is force the weather endpoint of the client to take an id of the location we want to fetch weather of: weather: { id in URLSession.shared .dataTaskPublisher( for: URL( string: "https://www.metaweather.com/api/location/\(id)" )! ) … },

2:15

This of course won’t compile because we need to update the weather client’s interface to take this id as input. public struct WeatherClient { public var weather: (Int) -> AnyPublisher<WeatherResponse, Error> public var searchLocations: (CLLocationCoordinate2D) -> AnyPublisher<[Location], Error> public init( weather: @escaping (Int) -> AnyPublisher<WeatherResponse, Error>, searchLocations: @escaping (CLLocationCoordinate2D) -> AnyPublisher< [Location], Error > ) { self.weather = weather self.searchLocations = searchLocations } }

2:30

Then we can update our mocks to ignore this value. extension WeatherClient { public static let empty = Self( weather: { _ in … ) public static let happyPath = Self( weather: { _ in … ) public static let failed = Self( weather: { _ in … ) }

2:45

To get one of these ids we will hit another endpoint of the API that lets us search locations near a given latitude and longitude.

3:02

If we open up a terminal window we can interact with this API and see what kind of data we’re working with. $ curl -Ls \ "https://www.metaweather.com/api/location/search?lattlong=40,-71" \ | python -m json.tool [ { "distance": 79406, "latt_long": "40.71455,-74.007118", "location_type": "City", "title": "New York", "woeid": 2459115 }, … ]

3:17

Firing off this API request returns an array of locations, each with a woeid field, which is exactly what we need to use in our request to the weather endpoint. “woeid” stands for “where on Earth identifier”, which is just the name of the id for the type of location database this API is using.

3:22

We’ve even already stubbed this endpoint out on our client: public var searchLocations: (CLLocationCoordinate2D) -> AnyPublisher<[Location], Error>

3:40

But to construct this API request we should update our Location model stub to be Decodable and add the fields we want to decode, in particular the woeid and the title , which we will render in the UI. struct Location: Decodable { var title: String var woeid: Int }

4:04

And now we can update the live client with the logic needed to fire off a location search.

4:09

We can copy the logic from the weather request and make a couple small changes: we’ll update the URL to the location search endpoint and interpolate the coordinate’s latitude and longitude into the URL, and we’ll change the decoded type to be an array of locations. searchLocations: { coord in URLSession.shared .dataTaskPublisher( for: URL( string: "https://www.metaweather.com/api/location/search?lattlong=\(coord.latitude),\(coord.longitude)" )! ) .map { data, _ in data } .decode(type: [Location].self, decoder: weatherJsonDecoder) .receive(on: DispatchQueue.main) .eraseToAnyPublisher() }

4:53

The changes we’ve made to the weather client have broken our feature code. In particular, when we attempt to fetch the weather: self.weatherClient .weather() Missing argument for parameter #1 in call

5:04

We must now pass along a location id, which we can get by performing a location search: self.weatherClient.searchLocations(<#CLLocationCoordinate2D#>) But in order to supply a coordinate here we must first get the user’s location out of Core Location.

5:09

So let’s hardcode this value for now just to get things building again. self.weatherClient .weather(2459115)

5:13

And now things should operate exactly as before, but we’re now in a position to introduce Core Location into our application. Using Core Location

5:36

Let’s describe how we want to integrate with Core Location in the application. At the bottom right corner of the app we have a location button, and when this button is tapped we want to request the user’s location.

5:52

In the view we will hook into the location button’s action closure to let the view model know whenever the button is tapped. Button( action: { self.viewModel.locationButtonTapped() } ) { Image(systemName: "location.fill") .foregroundColor(.white) }

6:02

And in the view model we will use this method to request the user’s current location, though this is made more complicated by the fact that we must be authorized to successfully perform the request in the first place. func locationButtonTapped() { }

6:08

Before we can request the user’s location we must first check if we are authorized to do so in the first place, and if not we must ask for permission. All of this is handled by the CLLocationManager class in Core Location, so we can start by invoking a class method that returns the authorization status and switching on it, where we can consider each case. func locationButtonTapped() switch CLLocationManager.authorizationStatus() { case .notDetermined: <#code#> case .restricted: <#code#> case .denied: <#code#> case .authorizedAlways: <#code#> case .authorizedWhenInUse: <#code#> @unknown default: <#code#> } }

6:42

If the authorization status is not determined, we need to request authorization, which we can do by instantiating a CLLocationManager . let locationManager = CLLocationManager() … case .notDetermined: locationManager.requestWhenInUseAuthorization() We will request the more limited “when-in-use” authorization since we only need to make location requests when the app is in use.

7:29

For the restricted and denied cases we can break for now, though this is where we should do some error handling. case .restricted: // TODO: show an alert break case .denied: // TODO: show an alert break

7:57

If we get an “authorized” status, we can fire off a location request. case .authorizedAlways, .authorizedWhenInUse: locationManager.requestLocation() We don’t request an “always” authorization at this time, but maybe in the future our application will periodically fetch the weather in the background, so we can handle both cases the same way here.

8:19

And we’ll simply break in the unknown case. @unknown default: break

8:24

Both requestWhenInUseAuthorization and requestLocation methods return Void . And this is because they are not synchronous and cannot synchronously return an authorization status or location. Instead, the location manager will asynchronously perform the request and let its delegate know the result some time later.

8:58

In order to get notified of authorization changes and location updates we can conform our view model to CLLocationManagerDelegate and implement the appropriate delegate methods. public class AppViewModel: ObservableObject, CLLocationManagerDelegate { Cannot declare conformance to ‘NSObjectProtocol’ in Swift; ‘AppViewModel’ should inherit ‘NSObject’ instead

9:11

And a location manager delegate must also be an NSObject subclass. public class AppViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {

9:19

Which requires us to call super in our initializer. super.init()

9:31

And now we can assign ourselves as the location manager’s delegate. let locationManager = CLLocationManager() … self.locationManager.delegate = self

10:11

Now we must implement a few delegate methods to be notified of the results of our requests.

10:14

We’ll start with locationManager(_:didChangeAuthorization:) , which is called by the manager when an authorization request completes. public func locationManager( _ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus ) { }

10:26

And in here we will switch on the status again, so that we can again consider all cases. public func locationManager( _ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus ) { switch status { case .notDetermined: <#code#> case .restricted: <#code#> case .denied: <#code#> case .authorizedAlways: <#code#> case .authorizedWhenInUse: <#code#> @unknown default: <#code#> } }

10:45

For the notDetermined case we can break this time, because if we get a change authorization notification and it goes back to “not determined” for some reason, there doesn’t seem to be anything we can do. case .notDetermined: break

11:04

For restricted and denied we will also break , but again we should probably be doing some error handling here. Important to note that we may want to show different errors here than what we would show as a result of the user tapping the location button. case .restricted: // TODO: show an alert break case .denied: // TODO: show an alert break

11:50

Finally, if authorization changes to be authorized, we can fire off a location request. case .authorizedAlways, .authorizedWhenInUse: self.locationManager.requestLocation()

12:02

And again, we can break in the unknown case since there’s not much we can do about that. @unknown default: break

12:05

Next we can implement the pair of delegate methods that can be called when a location is requested. One is for when the manager successfully returns an array of locations to the delegate: public func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { }

12:35

If the manager successfully returns some locations, we can grab the the first one to fire off a weather client location search. public func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { guard let location = locations?.first else { return } }

12:45

Then we can use it to perform a location search from the location’s coordinate. public func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { guard let location = locations.first else { return } self.weatherClient .searchLocations(location.coordinate)

13:05

And we can sink on the result. .sink( receiveCompletion: <#((Subscribers.Completion<Error>) -> Void)#>, receiveValue: <#(([Location]) -> Void)#> )

13:16

We can ignore the completion for now, but eventually we should handle any errors here. receiveCompletion: { _ in },

13:22

Successful requests will return an array of weather locations, in order of proximity to the request’s coordinate. receiveValue: { locations in }

13:54

We can introduce some state on our view model to hold onto the current location both to render the location’s title and to fetch its weather. @Published var currentLocation: Location? … receiveValue: { [weak self] locations in self?.currentLocation = locations.first }

14:32

And with this extra bit of view model state we can finally stop hardcoding the weather id of our weather request and instead use the current location, we just need to guard to unwrap it. func refreshWeather() { guard let currentLocation = self.currentLocation else { return } self.refreshWeatherCancellable = self.weatherClient .weather(currentLocation.woeid)

14:55

We also need to remember to refresh the weather when the current location changes. receiveValue: { [weak self] locations in self?.currentLocation = locations.first self?.refreshWeather() }

15:03

Before taking things for a spin we should also implement the didFailWithError delegate endpoint, which is called if anything goes wrong in Core Data and will crash our application if so. public func locationManager( _ manager: CLLocationManger, didFailWithError error: Error ) { }

15:28

If we run our preview and now we see an empty screen, which makes sense because we are no longer immediately fetching the results for New York. If we tap the location button…nothing happens. It turns out that Xcode previews do not seem to support Core Location at all right now, so I guess we’re going to need to build the entire application and run it in a simulator instead.

16:53

It is taking a bit of time to build the app since our live weather client dependency is quite slow to build. Already this is a little annoying and running contrary to the whole reason we modularized earlier. We would prefer to be able to just build our weather feature target, and not have to worry about the main application target. We want this because the weather feature builds the fewest number of dependencies to do its job, and it builds only the interfaces of its dependencies, not the actual implementations. However, this quirk of the Xcode preview is forcing us to build the entire application, that is throwing a wrench into our development cycle.

18:17

Now that the application is running we see that tapping the location button will prompt us for authorization, and if we tap “allow”, we get some weather results a lil bit later.

18:32

To make sure things are really working as we expect, we can make a small change to the UI to display the current location name as the navigation title. .navigationBarTitle( self.viewModel.currentLocation?.title ?? "Weather" )

18:50

And now when we run we can see that, sure enough, we get the weather for New York.

19:00

We can also change the simulator location to another environment, like say, Tokyo, and when we tap the location button again…well, it still fetching the weather for New York. We’re not sure why, but it seems that updating the location of a simulator is not always instantaneous. If we wait about a minute, though…

19:39

…and then press the location button again, we’ll see that the title changes to a new location and we get some different weather results. This seems to be a bug in the simulator, and who knows when it’ll get fixed. Controlling the location client

19:56

We’ve now done the bare minimum to integrate our app with Core Location and have something that just about works, but I have to admit it’s a bit of a mess.

20:06

First off, the mere fact of bringing in Core Location and dropping it into our feature made our Xcode preview useless. Core Location simply does not work in Xcode previews, which is a bit of a bummer.

20:22

That forced us to build our main application to test the feature, which was a bummer because it brings in all of the live dependencies, some of which are very slow to build, like the weather client.

21:00

But then we encountered a strange iOS simulator bug where trying to simulate a bunch of different locations has the cost of waiting for some cached device state to clear, which took about a minute each time.

21:12

So what we’re seeing here is we have a dependency, we haven’t controlled it, and now we also have some problems.

21:19

But even beyond all of those problems, there is a far bigger problem: none of the code involving Core Location is testable. We’ve only just begun to add location functionality to our application and we already have a pretty complex state machine on our hands. It’d be nice to get some coverage on various authorization and location paths but it is currently impossible to do so.

21:39

All of these problems can be fixed if we just employ the technique that we have previous done twice: we need to extract and control this dependency on Core Location. But how do we even begin?

21:59

Well let’s start by defining a new client struct dependency that can wrap a CLLocationManager under the hood. struct LocationClient { }

22:10

In here we want to contain all of the various endpoints that we hit on the CLLocationManager as closure properties. This includes accessing the authorization status, requesting authorization, and requesting the current location. struct LocationClient { var authorizationStatus: () -> CLAuthorizationStatus var requestWhenInUseAuthorization: () -> Void var requestLocation: () -> Void }

22:52

We might also be tempted to add an endpoint for setting the location manager’s delegate. var setDelegate: (CLLocationManagerDelegate) -> Void

23:13

And then we could feed the view model through this closure to any live, underlying manager.

23:18

That would technically work, but this is not a great approach because it will not let us fully control the dependency. As long as the view model acts as the delegate it will have full access to a live CLLocationManager in every single one of its delegate methods, which gives it unfettered access to the live, uncontrolled dependency. For example, if you were to access authorizationStatus from the location manager handed to a delegate method instead of the location client, you would be operating outside the purview of the client, which is not what we want.

24:03

So the client should not only wrap and control the location manager, it should also wrap and control the location manager’s delegate, and when delegate methods are called, the client can relay these events to the view model. We can even package these events up in Combine publishers, like we did with the path monitor’s update handler.

24:20

We might be tempted to upgrade requestWhenInUseAuthorization and requestLocation to return publishers of that notify us when the authorization status has changed or location has updated: var requestWhenInUseAuthorization: () -> AnyPublisher<CLAuthorizationStatus, Never> var requestLocation: () -> AnyPublisher<[CLLocation], Error>

24:45

But it’s not quite right to tie delegate callbacks directly to the manager methods that caused them. For example, didUpdateLocations is invoked not only for requestLocation , but also for startUpdatingLocation . In addition, multiple delegate methods may be called for a single manager method. For example, when the location is updated, both didUpdateLocations and didUpdateTo:from: are called. And so it does not seem right to tie individual manager methods to individual delegate methods.

25:21

So let’s go back to returning Void . var requestWhenInUseAuthorization: () -> Void var requestLocation: () -> Void

25:24

Instead we will carve out a dedicated section of our client for delegate methods. One way to capture them would be to define a publisher per delegate method: var didChangeAuthorization: AnyPublisher<CLAuthorizationStatus, Never> var didUpdateLocations: AnyPublisher<[CLLocation], Never> var didFailWithError: AnyPublisher<Error, Never>

25:58

Even better, we can bundle up all of these publishers into a single one that acts as the delegate. We just need a data structure that can describe each of these delegate methods and then we can define a single publisher that emits values of that type. We can model each method as an enum case: public struct LocationClient { … var delegate: AnyPublisher<DelegateEvent, Never> enum DelegateEvent { case didChangeAuthorization(CLAuthorizationStatus) case didUpdateLocations([CLLocation]) case didFailWithError(Error) } }

26:54

And this way when we subscribe to the delegate publisher we can even exhaustively switch on the event.

27:01

Let’s see what it takes to cook up a live implementation for this client, which will actually call out to Core Location APIs under the hood. We can start by adding a static property on the client type: extension LocationClient { public static var live: Self { } }

27:20

And in here we can construct a client to return: public static var live: Self { Self( authorizationStatus: <#() -> CLAuthorization#>, requestWhenInUseAuthorization: <#() -> Void#>, requestLocation: <#() -> Void#>, delegate: <#AnyPublisher<DelegateEvent, Never>#> ) }

27:32

Creating a delegate publisher will be a bit complicated, but the manager methods are quite simple to implement. We can construct a CLLocationManager and call it under the hood. public static var live: Self { let locationManager = CLLocationManager() return Self( authorizationStatus: { CLLocationManager.authorizationStatus() }, requestWhenInUseAuthorization: { locationManager.requestWhenInUseAuthorization() }, requestLocation: { locationManager.requestLocation() }, delegate: <#AnyPublisher<DelegateEvent, Never>#> ) }

28:05

We can even pass along its method references directly: public static var live: Self { let locationManager = CLLocationManager() return Self( authorizationStatus: CLLocationManager.authorizationStatus, requestWhenInUseAuthorization: locationManager .requestWhenInUseAuthorization, requestLocation: locationManager.requestLocation, delegate: <#AnyPublisher<DelegateEvent, Never>#> ) }

28:16

But how do we go about creating that delegate publisher? Like we did with the path update publisher we can introduce a passthrough subject and pass it along, erased. public static var live: Self { let locationManager = CLLocationManager() let subject = PassthroughSubject<DelegateEvent, Never>() return Self( authorizationStatus: locationManager.authorizationStatus, requestWhenInUseAuthorization: locationManager .requestWhenInUseAuthorization, requestLocation: locationManager.requestLocation, delegate: subject.eraseToAnyPublisher() ) }

28:41

This compiles but of course isn’t right because we’re never sending values to the subject.

28:50

What we need to do is create an actual delegate object that can receive the manager’s delegate methods. This object is only needed for the live client implementation, and so we can even hide it to only be available in the scope of our live client, which means it completely hidden from everyone on the outside: public static var live: Self { class Delegate: NSObject, CLLocationManagerDelegate { } … }

29:19

And in here we can implement all of the delegate methods we want to be notified of. func locationManager( _ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus ) { <#code#> } func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { <#code#> } func locationManager( _ manager: CLLocationManager, didFailWithError error: Error ) { <#code#> }

29:36

In here we need to somehow send the subject delegate events with the data. We can allow it to hold onto a subject, which requires us to also define an initializer. class Delegate: NSObject, CLLocationManagerDelegate { let subject: PassthroughSubject<DelegateEvent, Never> init(subject: PassthroughSubject<DelegateEvent, Never>) { self.subject = subject } … }

29:58

And then in each delegate method we can call self.subject.send with the data. func locationManager( _ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus ) { self.subject.send(.didChangeAuthorization(status)) } func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { self.subject.send(.didUpdateLocations(locations)) } func locationManager( _ manager: CLLocationManager, didFailWithError error: Error ) { self.subject.send(.didFailWithError(error)) }

30:23

Now we can create one of these Delegate s with our subject and assign it to the location manager. let locationManager = CLLocationManager() let subject = PassthroughSubject<DelegateEvent, Never>() let delegate = Delegate(subject: subject) locationManager.delegate = delegate

30:36

But the delegate property on location manager is weak so it will be immediately deallocated if we don’t capture it somewhere. What we want to do is somehow tie the delegate’s lifetime to the subject’s. We can do this by making the delegate property a mutable optional so that we can capture it and nil it out later on when the subject receives a cancellation event. var delegate: Delegate? = Delegate(subject: subject) locationManager.delegate = delegate return Self( delegate: delegate .handleEvents(receiveCancel: { delegate = nil } .eraseToAnyPublisher() … )

31:36

We have now created our third and final dependency, and it’s the most complicated by far, because it introduces the notion of delegates and captures a complex state machine. We found, though, that delegate methods are pretty perfectly captured in Combine publishers.

32:04

And this is how it goes with delegates: whenever we encounter a delegate in a dependency we want to control, we can bundle it up in a Combine publisher. And the once you learn the process it becomes quite mechanical. Using the location client

32:28

But we still need to integrate this controlled dependency into our feature, so let’s give it a shot.

32:42

First let’s swap out the view model’s location manager out for a location client. // let locationManager = CLLocationManager() let locationClient: LocationClient

32:58

And we won’t provide a default. Instead we will demand that whoever constructs the view model must supply a location client as a dependency. public init( locationClient: LocationClient, pathMonitorClient: PathMonitorClient, weatherClient: WeatherClient ) { self.locationClient = locationClient self.pathMonitorClient = pathMonitorClient self.weatherClient = weatherClient … }

33:28

Next, we no longer need to set ourselves as a delegate of a location manager, which means we no longer need to conform to CLLocationManagerDelegate or subclass NSObject , which in turn further cleans up the initializer. // public class AppViewModel: NSObject, ObservableObject, CLLocationManagerDelegate { public class AppViewModel: ObservableObject { … init(…) { … // super.init() … // self.locationManager.delegate = self

33:51

And then wherever we were using the location manager we can use the client instead: // switch CLLocationManager.authorizationStatus() switch self.locationClient.authorizationStatus() { … // self.locationManager.requestWhenInUseAuthorization() self.locationClient.requestWhenInUseAuthorization() … // self.locationManager.requestLocation() self.locationClient.requestLocation()

34:26

The location client now fully encapsulates delegate methods in a single Combine publisher, so in order to subscribe to that data we must sink on it. So we can add the following sink in the view model’s initializer: self.locationClient.delegate .sink { event in }

35:07

And in here we can simply switch on the event to handle each case. self.locationClient.delegate .sink { event in switch event { case .didChangeAuthorization: case .didUpdateLocations(_): case .didFailWithError(_): } }

35:17

And this is how we handle delegate methods in a world where they’ve been packaged up in a publisher. Instead of conforming to a protocol and implementing a bunch of methods, we can simply sink on a publisher, switch on the event, and then handle the logic in each case.

35:33

So let’s remove each delegate method by copying and pasting the body of each one into the associated case of the sink: self.locationClient.delegate .sink { [weak self] event in guard let self = self else { return } switch event { case let .didChangeAuthorization(status): switch status { case .notDetermined: break case .restricted: // TODO: show an alert break case .denied: // TODO: show an alert break case .authorizedAlways, .authorizedWhenInUse: self.locationClient.requestLocation() @unknown default: break case let .didUpdateLocations(locations): guard let location = locations.first else { return } self.searchLocationCancellable = self.weatherClient .searchLocations(location.coordinate) .sink( receiveCompletion: { _ in }, receiveValue: { [weak self] locations in self?.currentLocation = locations.first self?.refreshWeather() } ) case let .didFailWithError(error): break } }

36:20

But we have a warning that we’re not using the result of the sink, which is a cancellable that we must hold onto to ensure that we don’t immediately cancel the subscription. var locationDelegateCancellable: AnyCancellable? … self.locationDelegateCancellable = self.locationClient.delegate

36:47

In order to get the preview compiling we need to supply a location client, but rather than using the live client, which doesn’t work in Xcode previews and ideally would not even be accessible in this module, we can define another static mock, instead. We can start with one that simulates being in an authorized state. extension LocationClient { // public static func authorizedWhenInUse( // locatedAt: CLLocation = .init() // ) public static var authorizedWhenInUse: Self { Self( authorizationStatus: <#() -> CLAuthorizationStatus#>, requestWhenInUseAuthorization: <#() -> Void#>, requestLocation: <#() -> Void#>, delegate: <#AnyPublisher<DelegateEvent, Never>#> ) } }

37:58

We know we want to start in an authorized state, so we can return that from the authorizationStatus endpoint. authorizationStatus: { .authorizedWhenInUse },

38:15

We should never hit the authorization request path, so that can be an empty closure. requestWhenInUseAuthorization: { },

38:27

When the location is requested we want a delegate to emit some locations, so we can do what we’ve done in the past when we want to control a publisher, which is to introduce a subject that we can send locations to inside the endpoint. let subject = PassthroughSubject<DelegateEvent, Never> … return Self( requestLocation: { subject.send(.didUpdateLocations([CLLocation()]) }, delegate: subject.eraseToAnyPublisher()

39:20

We’re passing a location that doesn’t have any information, but if we wanted to control a particular latitude and longitude returned from Core Data this would be where we could do just that, because CLLocation s can be constructed by providing a coordinate.

39:46

Now we can supply this mock to the preview. struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView( viewModel: AppViewModel( locationClient: .authorizedWhenInUse, pathMonitorClient: .satisfied, weatherClient: .happyPath ) ) } }

40:33

And now we would expect to get some weather results as soon as our client is loaded, but when we run the preview it’s still in an empty state, but that’s because we forgot to upgrade the “happy path” weather client to return any locations. // Just([]) Just([Location(title: "New York", woeid: 1)])

41:00

And when things run and we tap the location button, we see some weather come back. This isn’t quite the behavior we want, though, because we are already in the authorized state and should ideally immediately fetch some weather. So when we first construct the view model we should ask the client if we’re authorized before kicking off a location request. if self.locationClient.authorizationStatus() == .authorizedWhenInUse { self.locationClient.requestLocation() }

41:56

And now when we run our preview we immediately get some weather results! This will make iterating on the feature in this state much easier, because we will not have to manually tap the location button to see what weather results look like.

42:28

But even better, we can cook up a mock that simulates the multi-step process of starting in a not-determined state, but when we tap the location button we flip to an authorized state. We just need to introduce some local, mutable state for the authorization status. extension LocationClient { public static var notDetermined: Self { var status = CLAuthorizationStatus.notDetermined let subject = PassthroughSubject<DelegateEvent, Never>() return Self( delegate: subject.eraseToAnyPublisher(), authorizationStatus: { status }, requestWhenInUseAuthorization: { status = .authorizedWhenInUse subject.send(.didChangeAuthorization(status)) }, requestLocation: { subject.send(.didUpdateLocations([CLLocation()])) } ) } }

44:21

And when we swap out our preview’s mock. // locationClient: .authorizedWhenInUse, locationClient: .notDetermined,

44:45

And now we get that behavior where we start out in an empty state, but tapping the location button will immediately populate the view with some weather. We could even take things further and have other “not-determined” mocks that simulate denying access.

45:12

This is super cool! We have fully captured the complex state machine of Core Location taking a user from a not-yet-authorized state to an authorized state and can see how this flow interacts with our feature, automatically populating the weather when authorization completes. And we were able to do so in just a few lines of code because we’ve built our dependencies to be constructible in such a simple manner. If we had used protocols instead it would have been a much more complicated feat to achieve.

45:48

The alternative of just piling all this core location code into the view model meant completely breaking our previews and required us to fully build the application to a device or simulator in order to exercise the feature. Further, whenever we want to test location-specific functionality, like authorization, we need to delete the application, reset its settings or our simulator, which can significantly slow our development cycle.

46:18

Instead, what we get to do here is run all of that logic directly in a preview, which just isn’t possible when using Core Location on its own. And we think the way we design our dependencies has made this far easier than it would have if we had used protocols.

46:40

And we’ve only scratched the surface of this client. There are still a ton of flows we can mock and test.

47:03

Alright! We have now fully controlled the location manager and its delegate events. But let’s also isolate it into its own module, which will allow us to reuse it in other targets, like a widget or watchOS extension. Rather than walk through the steps, as we did in the seconds episode of the series, let’s wave our magic wand one last time and skip to the final state.

47:44

✨🎩✨ And there it is, fully extracted into its own pair of LocationClient and LocationClientLive modules, and is fully imported into the feature and application. Next time: the point

48:37

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.

49:39

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.

50:00

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?

50:29

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…next time! References Dependency Inversion Principle A design pattern of object-oriented programming that flips the more traditional dependency pattern so that the implementation depends on the interface. We accomplish this by having our live dependencies depend on struct interfaces. https://en.wikipedia.org/wiki/Dependency_inversion_principle Downloads Sample code 0113-designing-dependencies-pt4 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 .