Find Your Location
One of the main reasons to use a mapping application is to find out where you are. The LocationEngine
provided by the HERE SDK implements a comprehensive location solution that uses the iOS platform positioning in the background, and works with several location sources such as GPS or other Global Navigation Satellite System (GNSS) receivers.
Note
At a glance
Integrating the HERE SDK location features requires at least the following steps:
- Add the required iOS permissions to your
.plist
file and request the permissions from the user. - Create a
LocationEngine
and set at least one LocationDelegate
. - Start the
LocationEngine
once and set the desired accuracy level. - Receive
Location
updates and handle them in your app.
Add the Required Permissions
Before you can start using the LocationEngine
in your app, you will need to add the required permissions to the app's Info.plist
file:
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>location-services</string>
<string>gps</string>
</array>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs to access your current location to display it on the map.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs to access your current location to display it on the map.</string>
<key>NSMotionUsageDescription</key>
<string>Motion detection is needed to determine more accurate locations, when no GPS signal is found or used.</string>
Note
Note that permission NSLocationAlwaysAndWhenInUseUsageDescription
is needed only if your application wants to request location updates while on background. See chapter Enable Background Updates for additional information.
An app using native location services such as GPS will ask for the user's permission. Not all devices provide the same capabilities and may have certain hardware restrictions that can lead to varying results. Prior to using the LocationEngine
, it may be a good idea to check if the native location services are enabled. On most iOS devices, a user can navigate to Settings > Privacy > Location Services to make sure that the location services are on.
You can use the code snippet below to check the application's CLAuthorizationStatus
and request the user authorization. Check the iOS documentation to find out more about Apple's CLAuthorizationStatus
.
import CoreLocation
private func startLocating() {
if locationEngine.start(locationAccuracy: .bestAvailable) == .missingPermissions {
requestLocationAuthorization()
}
}
public func requestLocationAuthorization() {
let locationAuthorizationStatus = CLLocationManager.authorizationStatus()
switch locationAuthorizationStatus {
case .restricted:
let alert = UIAlertController(title: "Location Services are restricted", message: "Please remove Location Services restriction in your device Settings", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
return
case .denied:
let alert = UIAlertController(title: "Location access is denied", message: "Please allow location access for the application in your device Settings", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
return
case .authorizedWhenInUse, .authorizedAlways:
break
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
break
default:
break
}
}
Create a LocationEngine
Creating a new LocationEngine
is simple:
do {
try locationEngine = LocationEngine()
} catch let engineInstantiationError {
fatalError("Failed to initialize LocationEngine. Cause: \(engineInstantiationError)")
}
Get the Last Known Location
Once the engine is initialized, the last known location can be obtained, as long as the engine has been started at least once before and received at least one position, otherwise nil
will be returned. This information will remain, so the last known location will also be available between application sessions.
if let myLastLocation = locationEngine.lastKnownLocation {
print("Last known location: '%f', '%f'", myLastLocation.coordinates.latitude, myLastLocation.coordinates.longitude)
}
Note
The LocationEngine
does not need to be started nor any listener needs to be set in order to get the last known location. It is enough that the LocationEngine
was successfully started once in a previous session and that a valid location event was received at least once. The Location
object contains a timestamp
that indicates when that location was received.
Get Notified on Location Events
Next before starting the LocationEngine
, it's a good idea to ensure that you will be notified of changes in the engine's status by conforming to the LocationStatusDelegate
protocol and register it with the location engine's addLocationStatusDelegate()
method. Check the API Reference for more information on the different statuses.
class PositioningExample: LocationStatusDelegate {
func onStatusChanged(locationEngineStatus: LocationEngineStatus) {
print("LocationEngineStatus: : \(locationEngineStatus)")
}
func onFeaturesNotAvailable(features: [LocationFeature]) {
for feature in features {
print("Feature not available: '%s'", String(describing: feature))
}
}
locationEngine.addLocationStatusDelegate(locationStatusDelegate: self)
}
Note
After a successful start, LocationStatusDelegate
will always receive status LocationEngineStatus.engineStarted
, and after a successful stop, it will always receive status LocationEngineStatus.engineStopped
.
Additionally, through the delegate's onFeaturesNotAvailable()
callback you will be notified of any LocationFeature
that is not available. If a feature that you need is not available, contact your HERE representative. Note: LocationFeature
enum is currently a pending feature.
The last thing to consider before starting the engine is conforming to the LocationDelegate
protocol, which provides the onLocationUpdated()
callback that sends a notification once a new Location
is detected. You can do so in a similar way as with the previously mentioned LocationStatusDelegate
:
class PositioningExample: LocationDelegate {
func onLocationUpdated(location: Location) {
print("Location updated: \(location.coordinates)")
}
locationEngine.addLocationDelegate(locationDelegate: self)
}
Note
The callback onLocationUpdated()
is received on the main thread - same as for all other callbacks.
Except for the current geographic coordinates and the timestamp, all other Location
fields are optional. For example, the received Location
object may contain the information about the bearing angle, as well as the current speed, but this is not guaranteed to be available. Unavailable values will be returned as nil
. What kind of sources are used for positioning (as defined by the LocationAccuracy
used to start the engine, see the Start and Stop Receiving Locations section below), and the device's capabilities affect what fields will be available.
You can add as many LocationStatusDelegate
and LocationDelegate
as you need by calling the respective addLocationStatusDelegate()
and addLocationDelegate
methods.
Start and Stop Receiving Locations
You are now ready to call the LocationEngine
's start()
method by passing in it one of the pre-defined LocationAccuracy
modes, as in the code snippet below:
class PositioningExample: LocationStatusDelegate, LocationDelegate {
private func startLocating() {
locationEngine.addLocationStatusDelegate(locationStatusDelegate: self)
locationEngine.addLocationDelegate(locationDelegate: self)
if locationEngine.start(locationAccuracy: .bestAvailable) == .missingPermissions {
requestLocationAuthorization()
}
}
}
Note
The start()
method already returns a LocationStatus
: Even though we have set a LocationStatusDelegate
in the line above, we consume the status immediately to check whether the location permission is granted for the application. If the permission is not granted, the OS will request permission from the user.
LocationEngine
uses the iOS platform positioning in the background to generate location updates. See the table below to understand how LocationAccuracy
maps to iOS' own CLLocationAccuracy
or check the API Reference for more information about all the available modes.
LocationAccuracy | CLLocationAccuracy |
bestAvailable | kCLLocationAccuracyBest |
navigation | kCLLocationAccuracyBestForNavigation |
tensOfMeters | kCLLocationAccuracyNearestTenMeters |
hundredsOfMeters | kCLLocationAccuracyHundredMeters |
kilometers | kCLLocationAccuracyThreeKilometers |
Table: Mapping of LocationAccuracy to CLLocationAccuracy. After the LocationEngine
has been started it remains in started state until you call stop()
on it. You will receive LocationEngineStatus.alreadyStarted
if you try to start it again without calling stop()
first. You can use the method isStarted()
to check if the engine is started or not. Similarly, if you have started a LocationEngine
and try to start another one without stopping the first, you will get LocationEngineStatus.alreadyStarted
error. Only one engine can be started at a time.
If you don't want to receive more location updates, you can stop the engine by calling the stop()
method. Remember to remove the delegates when they are no longer needed:
class PositioningExample: LocationStatusDelegate, LocationDelegate {
public func stopLocating() {
locationEngine.removeLocationDelegate(locationDelegate: self)
locationEngine.removeLocationStatusDelegate(locationStatusDelegate: self)
locationEngine.stop()
}
}
In general, it is recommended to stop the LocationEngine
when an app gets disposed.
Pause Updates while Stationary
By default, the LocationEngine
will automatically pause the location updates when location data is not expected to change. This can be used to improve battery life, for example when the device is stationary. This feature can be controlled by calling LocationEngine.setPauseLocationUpdatesAutomatically()
.
Enable Background Updates
In case you want to continue receiving location updates while the application is running in the background, you need to enable such capability by adding the following key to the app's Info.plist
file:
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
Additionally the user needs to be requested for autorization. The code snippet we shared in the above section Add the Required Permissions can also take care of that.
Once the autorization is cleared, location updates in the background are allowed by default. You can control that with the LocationEngine.setBackgroundLocationAllowed()
method. You can also set the visibility of the application's background location indicator with the method LocationEngine.setBackgroundLocationIndicatorVisible()
. It is visible by default.
Finally, you can ensure that the location updates won't pause when the device is stationary by passing false
to the LocationEngine.setPauseLocationUpdatesAutomatically()
method.
Note
setBackgroundLocationAllowed()
and setBackgroundLocationIndicatorVisible()
will return LocationEngineStatus.notAllowed
if the application does not have background location capabilities enabled. Otherwise, LocationEngineStatus.ok
will be returned.
Tutorial: Show your Current Location on a Map
A LocationIndicator
is used for representing device's current location on map. Before the indicator is updated with a current location value, a default Location
is set, which can be the last known location - or just any place the user should see before the first location update arrives.
Note
Currently, the LocationIndicator
does not support horizontal accuracy visualization, this feature is planned for a future HERE SDK update.
private static let defaultGeoCoordinates = GeoCoordinates(latitude: 52.520798, longitude: 13.409408)
private var locationIndicator: LocationIndicator!
private func addMyLocationToMap(myLocation: Location) {
locationIndicator = LocationIndicator()
locationIndicator.locationIndicatorStyle = .pedestrian;
locationIndicator.updateLocation(myLocation)
mapView.addLifecycleDelegate(locationIndicator)
mapCamera.lookAt(point: myLocation.coordinates,
distanceInMeters: PositioningExample.defaultCameraDistance)
}
private func updateMyLocationOnMap(myLocation: Location) {
locationIndicator.updateLocation(myLocation)
mapCamera.lookAt(point: myLocation.coordinates)
}
if let lastLocation = locationEngine.lastKnownLocation {
addMyLocationToMap(myLocation: lastLocation)
} else {
let defaultLocation = Location(coordinates: PositioningExample.defaultGeoCoordinates,
timestamp: Date())
addMyLocationToMap(myLocation: defaultLocation)
}
func onLocationUpdated(_ location: Location) {
updateMyLocationOnMap(myLocation: location)
}
Screenshot: Location indicator showing current location on map.
As shown in the implementation above, you can pass the Location
object to the location indicator by calling updateLocation()
. In this example, the goal is to track the user's current location - therefore, the map viewport's center location is updated as well.