Skip to content

18 - Core Location, Background

Core Location services

  • SwiftUI continuously creates and destroys views. When there is a state change, all views depending on it will be destroyed and recreated. This happens quite aggressively throughout the lifecycle of SwiftUI apps
  • SwiftUI provides us with specific property wrappers that store their values somewhere else rather than on the views directly.
    Two of such wrappers are @StateObject and @ObservedObject.
  • ViewModel will be responsible for receiving Core Location updates and providing info about location to view
  • Dealing with location requires user granted permission!

Minimal ViewModel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import Foundation
import CoreLocation

class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
    @Published var authorizationStatus: CLAuthorizationStatus

    private let locationManager: CLLocationManager

    override init() {
        locationManager = CLLocationManager()
        authorizationStatus = locationManager.authorizationStatus

        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.startUpdatingLocation()
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
struct ContentView: View {
    @StateObject var locationViewModel = LocationViewModel()

    var body: some View {
        switch locationViewModel.authorizationStatus {
        case .notDetermined:
            AnyView(RequestLocationView())
                .environmentObject(locationViewModel)
        case .restricted:
            ErrorView(errorText: "Location use is restricted.")
        case .denied:
            ErrorView(errorText: "The app does not have location permissions. Please enable them in settings.")
        case .authorizedAlways, .authorizedWhenInUse:
            TrackingView()
                .environmentObject(locationViewModel)
        default:
            Text("Unexpected status")
        }
    }
}

Requesting access to location

1
2
3
    func requestPermission() {
        locationManager.requestWhenInUseAuthorization()
    }

This causes dialog to pop-up where user can allow location access. If denied - can later be changed through settings.

If there are issues in simulator (app not visible in settings), try this from terminal to reset the permissions (relaunc app again after).

1
xcrun simctl privacy booted reset all

Also add callback to change authorization property in vm.

1
2
3
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    authorizationStatus = manager.authorizationStatus
}

RequestLocationView & ErrorView

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct RequestLocationView: View {
    @EnvironmentObject var locationViewModel: LocationViewModel

    var body: some View {
        VStack {
            Image(systemName: "location.circle")
                .resizable()
                .frame(width: 100, height: 100, alignment: .center)
                .foregroundColor(.blue)
            Button(action: {
                locationViewModel.requestPermission()
            }, label: {
                Label("Allow tracking", systemImage: "location")
            })
            .padding(10)
            .foregroundColor(.white)
            .background(Color.blue)
            .clipShape(RoundedRectangle(cornerRadius: 8))
            Text("We need your permission to track you.")
                .foregroundColor(.gray)
                .font(.caption)
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct ErrorView: View {
    var errorText: String

    var body: some View {
        VStack {
            Image(systemName: "xmark.octagon")
                .resizable()
                .frame(width: 100, height: 100, alignment: .center)
            Text(errorText)
        }
        .padding()
        .foregroundColor(.white)
        .background(Color.red)
    }
}

TrackingView

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct TrackingView: View {
    @EnvironmentObject var locationViewModel: LocationViewModel

    var body: some View {
        VStack {
            VStack {
                PairView(
                    leftText: "Latitude:",
                    rightText: String(coordinate?.latitude ?? 0)
                )
                PairView(
                    leftText: "Longitude:",
                    rightText: String(coordinate?.longitude ?? 0)
                )
                PairView(
                    leftText: "Altitude",
                    rightText: String(locationViewModel.lastSeenLocation?.altitude ?? 0)
                )
                PairView(
                    leftText: "Speed",
                    rightText: String("\(locationViewModel.lastSeenLocation?.speed ?? 0) m/s")
                )
                PairView(
                    leftText: "Distance",
                    rightText: String(locationViewModel.distance)
                )
            }
            .padding()
        }
    }

    var coordinate: CLLocationCoordinate2D? {
        locationViewModel.lastSeenLocation?.coordinate
    }
}

EnvironmentObject

@EnvironmentObject is a simpler way of using @ObservedObject on multiple sub-views. Rather than creating some data in view A, then passing it to view B, then view C, then view D before finally using it, create data in view A and put it into the environment so that views B, C, and D will automatically have access to it.

Like @ObservedObject, never assign a value to an @EnvironmentObject property. Instead, it should be passed in from elsewhere, and ultimately initialized with @StateObject to create it .

Environment objects must be supplied by an ancestor view – if SwiftUI can’t find an environment object of the correct type you’ll get a crash. This applies for previews too!

Receive location updates

Full viewmodel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class LocationViewModel: NSObject, Combine.ObservableObject, CLLocationManagerDelegate {
    @Published var authorizationStatus: CLAuthorizationStatus
    @Published var lastSeenLocation: CLLocation?

    @Published var distance: Double = 0


    private let locationManager: CLLocationManager

    override init() {
        locationManager = CLLocationManager()
        locationManager.allowsBackgroundLocationUpdates = true

        authorizationStatus = locationManager.authorizationStatus

        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.startUpdatingLocation()
    }

    func requestPermission() {
        locationManager.requestWhenInUseAuthorization()
    }

    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        authorizationStatus = manager.authorizationStatus
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let lastSeenLocation = lastSeenLocation, let curLocation = locations.first {
            distance = distance + lastSeenLocation.distance(from: curLocation)
        }

        lastSeenLocation = locations.first
    }

}

NB! You can include as many optional bindings and Boolean conditions in a single if statement as you need to, separated by commas. If any of the values in the optional bindings are nil or any Boolean condition evaluates to false, the whole if statement’s condition is considered to be false.

1
2
3
if let lastSeenLocation = lastSeenLocation, let curLocation = locations.first {
            distance = distance + lastSeenLocation.distance(from: curLocation)
}

is the same as

1
2
3
4
5
if let lastSeenLocation = lastSeenLocation {
    if let curLocation = locations.first {
        distance = distance + lastSeenLocation.distance(from: curLocation)
    }
}

PairView

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct PairView: View {
    let leftText: String
    let rightText: String

    var body: some View {
        HStack {
            Text(leftText)
            Spacer()
            Text(rightText)
        }
    }
}

App settings for location usage

Depending on location usage needed, these keys are mandatory in Info.plist (xml file with app settings).
Values are used to inform user in permission granting dialog, why app needs location access.

1
2
3
NSLocationAlwaysAndWhenInUseUsageDescription 
NSLocationWhenInUseUsageDescription
NSLocationAlwaysUsageDescription

NB! Info.plist was removed in XCode 13. Add keys via XCode UI.

info

Receiving location updates while in background

Keepin app running in background is extremely limited and controlled in iOS. Luckiliy, location updates is one of the features that is allowed.

Set up neccessary settings in XCode / Info.plist. Navigate to capabilities section and click "+" on upper right corner.

Choose Bakground Modes - new section is addedd to settings.

info

Activate Location updates.

info

also, you need to

1
locationManager.allowsBackgroundLocationUpdates = true

and

1
2
3
func requestPermission() {
    locationManager.requestAlwaysAuthorization()
}

Now when app starts to listen for location updates while in foreground and then enters background - app is kept running.

  • https://developer.apple.com/documentation/corelocation/requesting_authorization_for_location_services
  • https://developer.apple.com/documentation/corelocation/cllocationmanager/1620551-requestalwaysauthorization
  • https://developer.apple.com/documentation/corelocation/getting_the_user_s_location/using_the_standard_location_service
  • https://developer.apple.com/documentation/corelocation/getting_the_user_s_location/handling_location_events_in_the_background

Using Apple Maps - MapKit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import MapKit

...


@State private var region = MKCoordinateRegion(
    center: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275), 
    span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))


var body: some View {
        VStack {
            VStack {

                Map(
                    coordinateRegion: $region, 
                    showsUserLocation: true, 
                    userTrackingMode: .constant(.follow)
                )


            }
            .padding()
        }
    }

This is simple and really limited map view. No polylines etc. Has only simple annotations

  • MapPin
  • MapMarker
  • MapAnnotation

Advanced Map View - MKMapView

Create custom SwiftUI view based on older UIView, protocol UIViewRepresentable

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct SomeUIViewBasedView: UIViewRepresentable {
    @binding var someValue: SomeValueType

    func makeUIView(context: Context) -> UIViewOfSomeKind {
        return UIViewOfSomeKind()
    }

    func updateUIView(_ view: UIViewOfSomeKind, context: Context) {
        view.someAttribute = someValue
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    let region: MKCoordinateRegion
    let lineCoordinates: [CLLocationCoordinate2D]

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        mapView.region = region
        mapView.showsUserLocation = true

        let polyline = MKPolyline(coordinates: lineCoordinates, count: lineCoordinates.count)
        mapView.addOverlay(polyline)

        return mapView
    }

    func updateUIView(_ view: MKMapView, context: Context) {
        view.setRegion(region, animated: true)
        let polyline = MKPolyline(coordinates: lineCoordinates, count: lineCoordinates.count)
        view.addOverlay(polyline)
        view.removeOverlays(view.overlays.dropLast())
    }
}

class Coordinator: NSObject, MKMapViewDelegate {
    var parent: MapView

    init(_ parent: MapView) {
        self.parent = parent
    }

    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if let routePolyline = overlay as? MKPolyline {
            let renderer = MKPolylineRenderer(polyline: routePolyline)
            renderer.strokeColor = UIColor.systemBlue
            renderer.lineWidth = 10
            return renderer
        }
        return MKOverlayRenderer()
    }

}
  • https://developer.apple.com/documentation/mapkit/mkmapview
  • https://developer.apple.com/library/archive/samplecode/Breadcrumb/Introduction/Intro.html#//apple_ref/doc/uid/DTS40010048-Intro-DontLinkElementID_2

Using Google maps in SwiftUI

https://developers.google.com/codelabs/maps-platform/maps-platform-ios-swiftui