19 - 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
| 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).
| xcrun simctl privacy booted reset all
|
Also add callback to change authorization property in vm.
| 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.
| if let lastSeenLocation = lastSeenLocation, let curLocation = locations.first {
distance = distance + lastSeenLocation.distance(from: curLocation)
}
|
is the same as
| 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.
| NSLocationAlwaysAndWhenInUseUsageDescription
NSLocationWhenInUseUsageDescription
NSLocationAlwaysUsageDescription
|
NB! Info.plist was removed in XCode 13. Add keys via XCode UI.

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.

Activate Location updates.

also, you need to
| locationManager.allowsBackgroundLocationUpdates = true
|
and
| func requestPermission() {
locationManager.requestAlwaysAuthorization()
}
|
Now when app starts to listen for location updates while in foreground and then enters background - app is kept running.
Links
- 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
| 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()
}
}
|
Links for MKMapView
- 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