Skip to main content

Location Tracker in Swift

·4 mins

A Hands-On Tutorial using SwiftUI, Core Location, Combine and Core Data #

This post is building upon my previous post “Where is my iPhone” by using more Apple frameworks to build a working app for iPhone and iPad.

In addition to “just” determining the location of the device, I will lay out a full data flow pipeline from data origin, via persistence to visualisation.

Four Apple frameworks will be covered in this article:

  • Core Location
  • Combine
  • Core Data
  • SwiftUI

The order in which I am going through the frameworks resembles the order of the data flow.

The location coordinates originate in the Core Location framework and will be stored in a Core Data database.

The link between these two is a Combine Publisher which will receive the coordinates from Core Location and send them to Core Data.

Finally, the SwiftUI-view, which is linked to Core Data, will visualise all coordinates from the database as blue dots in a map.

The complete Xcode-Workspace with the fully-functional application can be downloaded from here.

Core Location #

Everything starts with an instance of CLLocationManager that is being instantiated in class LocationPublisher.

private let locationManager = CLLocationManager()

It needs a delegate assigned to it, which will be the LocationPublisher.

self.locationManager.delegate = self

In order to get the location updates, a delegate method needs to be implemented:

extension LocationPublisher: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }

        wrapped.send((longitude: location.coordinate.longitude, latitude: location.coordinate.latitude))
    }
}

The location coordinates are being sent to our Combine Publisher, which is described in the next section.

Before we start the app, a few keys need to be set in the Info.plist, otherwise no tracking will be possible.

Upon the first start of the app, you will be asked to confirm if Location Tracker is allowed to determine the users location.

It is time to “push” the coordinates into the database. We do so by using a Combine Publisher, which will “automagically” link both, the CLLocationManager and CoreData.

Combine #

The LocationPublisher has a PassthroughSubject which takes a tuple. The first value of the tuple is the longitude, the second the latitude of the received coordinate.

typealias Output = (longitude: Double, latitude: Double)
typealias Failure = Never

private let wrapped = PassthroughSubject<(Output), Failure>()

As we have seen in section “Core Location”, whenever a coordinate is received in the delegate method locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]), it will be send to the PassthroughSubject.

wrapped.send((longitude: location.coordinate.longitude, latitude: location.coordinate.latitude))

It’s time for the LocationPublisher to jump into action and actually become a Combine Publisher. This is needed in order for other classes which act as data sinks (Core Data anyone?) to subscribe to our publisher.

extension LocationPublisher: Publisher {
    func receive<Downstream: Subscriber>(subscriber: Downstream) where Failure == Downstream.Failure, Output == Downstream.Input {
        wrapped.subscribe(subscriber)
    }
}

This code is everything that is needed to provide a method that receives the coordinates that have been sent to the PassthroughSubject.

locationPublisher.sink(receiveValue:)

This is important in the next step, when we connect the output of the LocationPublisher with our CoreData-database.

Core Data #

To “push” the location data into the database, both, the LocataionPublisher and CoreData need to be connected.

The PersistenceController is the class which handles all our database related code.

It has a method called add which takes the tuple of longitude and latitude and saves it to the database.

This tuple will be converted into an entity called Location that has been defined in LocationTracker.xcdatamodel

Linking both, the LocationPublisher and the PersistenceController is a matter of just one-line, thanks to Combine.

locationPublisher.sink(receiveValue: PersistenceController.shared.add)

This happens right at the beginning when the application is being initialised in LocationTrackerApp.init.

Now, that the location data is being pushed into our database, how does it get visualised in a map?

Onto the next chapter …

SwiftUI #

The user interface consists of a map and a button.

The map shall display all coordinates that have been stored in CoreData.

Upon pressing the button, all data in the database will be deleted and the map automatically cleared from the markers.

In the ContentView, the locations will automatically be updated by using a @FetchRequest.

@FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Location.timestamp, ascending: true)],
        animation: .default)
private var locations: FetchedResults<Location>

What is only left, is telling the map to use locations.

Map(coordinateRegion: $region,
    interactionModes: .all,
    showsUserLocation: false,
    userTrackingMode: .constant(.follow),
    annotationItems: locations) { location in

    MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)) {
        Circle().fill(Color.blue).frame(width: 10, height: 10)
    }
}

As a result, every location is being displayed as a blue dot and with the map always being in sync with the database. No further code required. Seriously.

Conclusion #

And thats about it on how to build a location tracker with Apples latest frameworks.

I hope you have enjoyed this tutorial and got some helpful ideas for your project.

In case you prefer a ready-made solution, the complete Xcode-Workspace with the fully-functional application can be downloaded from here.

Resources #