Article
A Core Location Abstraction Layer with Combine and SwiftUI
June 7, 2021
One of the benefits of modern mobile devices is that they can determine your device’s precise location. This enables you to get directions to where you’re going, see nearby restaurants, or view a hyperlocal weather forecast.
On iOS, the Core Location framework gives a developer access to the device’s geographic location and orientation. At an initial glance, this makes it easy to determine the device’s location, with great accuracy.
But as you start to dig into the Core Location documentation, you’ll find that there are a few steps to the process. You have to first request the user’s permission to access the device’s location. Then, if granted, you can request the location. Since determining the device’s location isn’t always instantaneous, you’ll get a callback once a location has been determined.
The Core Location framework uses delegation to asynchronously respond to requests for asking for permission and determining location. Let’s see how this works by implementing a simple SwiftUI view which contains a button for requesting the device’s location.
Since fetching a location is asynchronous, we will show a ProgressView
while the location is being determined. Once a location is available, we either display a description of the location or an error message if a location wasn’t able to be determined.
Now we can create the view model which handles interacting with a CLLocationManager
to get the device’s location.
There’s a lot to this! The view model exposes properties for a loading indicator and the location or an error if the location is unable to be determined. Additionally, there’s a function for fetching the device’s location which requests permissions if necessary and then finally requests the device’s location.
While this solves the need of getting the device’s location, the logic is spread throughout the class. This isn’t terrible to follow for now, but this is a simple example and the view model is only doing one thing. As the requirements for this view grow (and the code that follows), it’s likely that the view model will become more complex, making it harder to understand and troubleshoot if there’s a bug.
We can abstract some of this location logic so that it’s encapsulated and reusable outside of this view.
We’ve moved all of the logic for requesting permissions and location into this LocationService
class. Instead of using a delegate to communicate with LocationService
, we’ve implemented Combine’s Futures .
A Future is a publisher that will eventually either produce a single value or an error. It is initialized with a completion closure that is called at some point in the future when the operation completes and either a value or an error is available. We can create a Future that is generic over CLLocation
and LocationError
which will either publish a CLLocation
if a location is determined or a LocationError
if there was an error fetching the location. Here’s the full Future declaration with the completion.
If we happen to already have or know the value without needing to perform an async operation, we can call the closure immediately. This is used in the LocationService
if we know the user has previously denied authorization to get the devices location, but we still want to return a Future.
Using a Future here is great for handling these asynchronous operations because it keeps the logic at the call site compact by allowing us to chain multiple operations together. Here’s an updated view model that implements our new LocationService
.
The API for our view model hasn’t changed, so we can integrate this with our original MyLocationView
without any changes. Now we have all of the location logic in one compact method. We start by requesting permission to get the device’s location while our app is in use, and once that succeeds, we request the device’s location. Any failures that occur in the chain will be handled in the receiveCompletion
closure of the sink
method.
This gives us a lot of flexibility. If we wanted to get the device’s location on another view in the app, we no longer need to duplicate the delegate code within each view model. Additionally, LocationService
isn’t specific to this application, so we could reuse it in other apps that we are building and extend it to handle other location requirements. And switching from using a delegate-based API to one that uses Futures and Promises improves on the readability of the code within the view model.
While Core Location gives us a good testbed for demonstrating this technique, this approach can be used with other frameworks and even custom code that you write in your application. Hopefully this article has given you an idea of how you can abstract common logic into a helper or utility class to improve on readability and conciseness within your app.
Matt works at Livefront where he builds iOS applications with abstraction layers to make his life easier.