"didUpdateLocations" doesn't being called when monitoring significant location changes - ios

I'm trying to monitor user's significant location changes using a singleton locationManager called LocationService, it goes like that:
LocationService.Swift:
class var sharedInstance: LocationService {
struct Static {
static var onceToken: dispatch_once_t = 0
static var instance: LocationService? = nil
}
dispatch_once(&Static.onceToken) {
Static.instance = LocationService()
}
return Static.instance!
}
var locationManager: CLLocationManager?
var location: CLLocation?
var delegate: LocationServiceDelegate?
override init() {
super.init()
self.locationManager = CLLocationManager()
guard let locationManager = self.locationManager else {
return
}
if CLLocationManager.authorizationStatus() == .NotDetermined {
locationManager.requestAlwaysAuthorization()
}
locationManager.delegate = self
locationManager.distanceFilter = 10
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.pausesLocationUpdatesAutomatically = false
if NSString(string: UIDevice.currentDevice().systemName).floatValue >= 9 {
locationManager.allowsBackgroundLocationUpdates = true
}
}
func startUpdatingLocation() {
print("Starting Location Updates")
self.locationManager?.startUpdatingLocation()
}
func startMonitoringSignificantLocationChanges() {
print("Starting Significant Location Updates")
self.locationManager?.startMonitoringSignificantLocationChanges()
}
// CLLocationManagerDelegate
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else {
return
}
// singleton for get last location
self.location = location
}
myVC.swift:
private let locationService = LocationService.sharedInstance
func prepareInformation() {
self.locationService.delegate = self
self.locationService.startMonitoringSignificantLocationChanges()
}
but didUpdateLocations being called just one time when the app is launched, and then it doesn't even being called. But when I switch the line:
self.locationService.startMonitoringSignificantLocationChanges()
to:
self.locationService.startUpdatingLocation()
it works great and called every time the user moves.
what can be the problem? Thank you!

According to Apple docs
After returning a current location fix, the receiver generates update
events only when a significant change in the user’s location is
detected. It does not rely on the value in the distanceFilter property
to generate events.
And
Apps can expect a notification as soon as the device moves 500 meters
or more from its previous notification. It should not expect
notifications more frequently than once every five minutes. If the
device is able to retrieve data from the network, the location manager
is much more likely to deliver notifications in a timely manner
I think you are expecting an event when the user changes its location by 10 meters, but this method doesn't depend on distanceFilter. Whoever when you use startUpdatingLocation() you will get events which will depend on distanceFilter property.
You can read more about this here.

Related

Getting location on real device not working

I'm trying to get the user location, running on the simulator, I get the default address, but atleast I know it is working.
I tried to run it on my device but it didn't work.
I try to look for a solution before writing this question but couldn't find something that work for me.
This is my code:
LocationManager:
class LocationManager: NSObject, CLLocationManagerDelegate {
static let shared = LocationManager()
var locationManager: CLLocationManager!
var geoCoder = CLGeocoder()
var callBack:((String)->())?
override init() {
super.init()
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.activityType = .other
locationManager.requestWhenInUseAuthorization()
}
func checkIfLocationIsEnabled() -> Bool{
return CLLocationManager.locationServicesEnabled()
}
func getUserLocation(){
locationManager.requestLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]){
let userLocation: CLLocation = locations[0] as CLLocation
geoCoder.reverseGeocodeLocation(userLocation) { (placemarks, err) in
if let place = placemarks?.last{
self.callBack?(place.name!)
}
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
}
}
This is my getLocation (just calling the getUserLocation and setting the address I get from the callback):
func getLocation(_ label: UILabel) -> String{
guard let comment = self.mView.addCommentTextField.text else { return ""}
LocationManager.shared.getUserLocation()
var addressString = ""
LocationManager.shared.callBack = { address in
DispatchQueue.main.async {
label.text = "\(address), \(comment)"
addressString = address
}
}
return addressString
}
This is how I call getLocation:
self.mView.inLabel.isHidden = false
self.getLocation(self.mView.inLabel)
Actually looking closer at your code, I see that you are asking permissions like this:
locationManager.requestWhenInUseAuthorization()
But requestWhenInUseAuthorization() is asynchronous call, you need to wait for user response before you can use any location services:
When the current authorization status is CLAuthorizationStatus.notDetermined, this method runs asynchronously and prompts the user to grant permission to the app to use location services.
(source)
Also notice that it will only work if status is notDetermined. Any other status would not trigger it. So firstly:
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
// already authorized, can use location services right away
}
if CLLocationManager.authorizationStatus() == .notDetermined {
locationManager.requestWhenInUseAuthorization()
// wait, don't call any location-related functions until you get a response
}
If location permissions are set to anything else, no point to ask for them.
And then your class is already CLLocationManagerDelegate, so:
func locationManager(_ manager: CLLocationManager,
didChangeAuthorization status: CLAuthorizationStatus) {
// do something with new status, e.g.
if status == .authorizedWhenInUse {
// good, now you can start accessing location data
}
// otherwise, you can't

Retrieve current location when application is in background

I've built an application where you can press a start button. Once the button is pressed the application will get user location every 10 second all the way till the stop button is pressed. When I leave the application or if the screen gets black, it will NOT get any more locations till I re-enter the application.
So, I'm currently trying to update the locations when the application is minimized. (I guess it's called in the background?), and also when the screen turns black. But my questions is:
Should I write this code in the AppDelegate?, if so. How can I know
if the button was pressed or not?
Where exactly in the AppDelegate should I add the code? And how can
I pass the locations back to the correct ViewController? (Since I
cannot make any prepare for segue from AppDelegate)
If you know the answers of this questions, please do not hesitate to answer them. :) I would really appreciate it!
The best way to get user's location in background is to use the Significant-Change Location Service according to apple's documentation put this func in your class:
func startReceivingSignificantLocationChanges() {
let authorizationStatus = CLLocationManager.authorizationStatus()
if authorizationStatus != .authorizedAlways {
// User has not authorized access to location information.
return
}
if !CLLocationManager.significantLocationChangeMonitoringAvailable() {
// The service is not available.
return
}
locationManager.delegate = self
locationManager.startMonitoringSignificantLocationChanges()
}
and also this func:
func locationManager(_ manager: CLLocationManager, didUpdateLocations
locations: [CLLocation]) {
let lastLocation = locations.last!
// Do something with the location.
}
so you just need to call startReceivingSignificantLocationChanges() inside your button and it will call locationManager(_ manager: CLLocationManager,didUpdateLocations locations: [CLLocation]), so do what you want with the location there.
Remember to ask permission to use location and to stop tracking with locationManager.stopMonitoringSignificantLocationChanges()
Take location permission for Always Allow
Set location manager for allowsBackgroundLocationUpdates true
from the above way you can get location in every location changes store this information and it send to server. Below is the sample code
typealias LocateMeCallback = (_ location: CLLocation?) -> Void
/*
LocationTracker to track the user in while navigating from one place to other and store new locations in locations array.
**/
class LocationTracker: NSObject {
static let shared = LocationTracker()
var lastLocation: CLLocation?
var locations: [CLLocation] = []
var previousLocation: CLLocation?
var isPreviousIsSameAsCurrent: Bool {
if let previous = previousLocation, let last = lastLocation {
return previous == last
}
return false
}
var isAggressiveModeOn = false
var locationManager: CLLocationManager = {
let locationManager = CLLocationManager()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = true
locationManager.activityType = .automotiveNavigation
return locationManager
}()
var locateMeCallback: LocateMeCallback?
var isCurrentLocationAvailable: Bool {
if lastLocation != nil {
return true
}
return false
}
func enableLocationServices() {
locationManager.delegate = self
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
// Request when-in-use authorization initially
locationManager.requestWhenInUseAuthorization()
case .restricted, .denied:
// Disable location features
print("Fail permission to get current location of user")
case .authorizedWhenInUse:
// Enable basic location features
enableMyWhenInUseFeatures()
case .authorizedAlways:
// Enable any of your app's location features
enableMyAlwaysFeatures()
}
}
func enableMyWhenInUseFeatures() {
locationManager.startUpdatingLocation()
locationManager.delegate = self
escalateLocationServiceAuthorization()
}
func escalateLocationServiceAuthorization() {
// Escalate only when the authorization is set to when-in-use
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
locationManager.requestAlwaysAuthorization()
}
}
func enableMyAlwaysFeatures() {
enableCoarseLocationFetch()
locationManager.startUpdatingLocation()
locationManager.delegate = self
}
// Enable Rough Location Fetch
func enableCoarseLocationFetch() {
isAggressiveModeOn = false
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
locationManager.distanceFilter = 100
}
// Enable Aggressive Location Fetch
func enableAggressiveLocationFetch() {
isAggressiveModeOn = true
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
locationManager.distanceFilter = 10
}
func locateMe(callback: #escaping LocateMeCallback) {
self.locateMeCallback = callback
if lastLocation == nil {
enableLocationServices()
} else {
callback(lastLocation)
}
}
func startTracking() {
enableLocationServices()
}
func stopTracking() {
locationManager.stopUpdatingLocation()
}
func resetPreviousLocation() {
previousLocation = nil
}
private override init() {}
}
extension LocationTracker: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print(locations)
guard let location = locations.first else { return }
guard -location.timestamp.timeIntervalSinceNow < 120, // Validate only location fetched recently
location.horizontalAccuracy > 0, // Validate Horizontal Accuracy - Ve means Invalid
location.horizontalAccuracy < 200 // Validate Horizontal Accuracy > 100 M
else {
print("invalid location received OR ignore old (cached) updates")
return
}
self.locations.append(location)
lastLocation = location
if let activeRide = RideManager.shared.activeRide,
let _ = AccessTokenHelper.shared.accessToken,
let activeRideId = activeRide.ride_id,
let type = activeRide.rideStatusTypeOptional,
type == .started {
//Store Location For A particular Ride after Start
LocationUpdater.shared.saveInDataBase(rideId: activeRideId, locations: [location])
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
enableLocationServices()
}
}
/*
This class having responsibility of Updating the location on server after n second and update path after n second.
**/
class LocationTimer {
static let time: Double = 30
}
/*
class to update locations to server after nth second
**/
class LocationUpdater: NSObject {
static let shared = LocationUpdater(n: Double(LocationTimer.time), tracker: LocationTracker.shared)
let n: Double
private let tracker: LocationTracker
var timer: Timer! = nil
init(n: Double, tracker: LocationTracker) {
self.n = n
self.tracker = tracker
super.init()
}
func startUpdater() {
self.timer?.invalidate()
self.timer = nil
self.timer = Timer.scheduledTimer(timeInterval: n, target: self, selector: #selector(updateLocationsToServer), userInfo: nil, repeats: true)
self.timer.fire()
}
func stopUpdater() {
self.timer?.invalidate()
self.timer = nil
}
#objc func updateLocationsToServer() {
// update to server
}
}
// usage
LocationTracker.shared.startTracking()
LocationUpdater.shared.startUpdater()

Location does not update after application comes back from background

I am building an app where I want to keep track of updated user location whenever the app comes back from background.
I wrote my location tracking code in AppDelegate's didFinishLaunchingWithOptions method
//Core Location Administration
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 70
locationManager.requestAlwaysAuthorization()
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.startMonitoringVisits()
locationManager.delegate = self
Since I was not able to validate Visits, I added the standard location tracking too
locationManager.allowsBackgroundLocationUpdates = true
locationManager.startUpdatingLocation()
I created CLLocationManagerDelegate block and added the following code
extension AppDelegate: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) {
let clLocation = CLLocation(latitude: visit.coordinate.latitude, longitude: visit.coordinate.longitude)
// Get location description
AppDelegate.geoCoder.reverseGeocodeLocation(clLocation) { placemarks, _ in
if let place = placemarks?.first {
let description = "\(place)"
self.newVisitReceived(visit, description: description)
}
}
}
func newVisitReceived(_ visit: CLVisit, description: String) {
let location = Location(visit: visit, descriptionString: description)
LErrorHandler.shared.logInfo("\(location.latitude), \(location.longitude)")
UserModal.shared.setUserLocation(location)
UserModal.shared.userLocationUpdated = Date()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else {
return
}
locationManager.stopUpdatingLocation()
let uL = Location(location:location.coordinate, descriptionString: "")
LErrorHandler.shared.logInfo("\(uL.latitude), \(uL.longitude)")
UserModal.shared.setUserLocation(uL)
UserModal.shared.userLocationUpdated = Date()
}
}
I added this code to begin location tracking when the app comes to foreground
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
locationManager.startUpdatingLocation()
}
If I am on a ViewController and the application goes back to the background, it does not refresh the location when the app comes to foreground.
Can someone suggest a better way to do this?
You should write code for location in didbecomeActive method of App delegate not in didFinishLaunchingWithOptions. Try this hope it will help.

Swift - Location Prompt not happening soon enough

I am building a location-based app that lists nearby coffee houses. App keeps crashing on first build on device because location keeps returning as nil.
This is because the Privacy - Location prompt isn't happening soon enough, even though though the request is earlier in the code. After I close the app after it crashes, that's when I'm prompted to allow my location.
I have three onboarding screens, and when I get to this tableviewcontroller, that's when it crashes.
If I go into Settings > Privacy > Location and manually enable location services, the app works great.
Here's my code (I removed a ton of unnecessary stuff):
import UIKit
import MapKit
import CoreLocation
class ShopTableViewController: UITableViewController, CLLocationManagerDelegate {
#IBAction func filterBack(_ sender: Any) {
getLocale()
shops.sort() { $0.distance < $1.distance }
shops.removeAll()
loadShops()
sortList()
}
//MARK: Properties
var shops = [CoffeeShop]()
var filteredShops = [CoffeeShop]()
var objects: [CoffeeShop] = []
var locationManager = CLLocationManager()
func checkLocationAuthorizationStatus() {
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
locationManager.requestWhenInUseAuthorization()
}
}
var currentLocation = CLLocation!.self
var userLatitude:CLLocationDegrees! = 0
var userLongitude:CLLocationDegrees! = 0
var locValue:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 1.0, longitude: 1.0)
var refresher: UIRefreshControl! = UIRefreshControl()
func getLocale() {
self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
self.locationManager.startMonitoringSignificantLocationChanges()
userLatitude = self.locationManager.location?.coordinate.latitude
userLongitude = self.locationManager.location?.coordinate.longitude
print("\(userLatitude), \(userLongitude)")
}
override func viewDidLoad() {
super.viewDidLoad()
self.locationManager = CLLocationManager()
/// self.locationManager.requestWhenInUseAuthorization()
checkLocationAuthorizationStatus()
self.locationManager.delegate = self
self.locationManager.startUpdatingLocation()
if CLLocationManager.locationServicesEnabled()
{
getLocale()
}
let locValue = self.locationManager.location?.coordinate
noHeight()
loadShops()
sortList()
print("\(locValue?.latitude), \(locValue?.longitude)")
refresher = UIRefreshControl()
refresher.addTarget(self, action: #selector(ShopTableViewController.handleRefresh), for: UIControlEvents.valueChanged)
if #available(iOS 10, *) {
shopTable.refreshControl = refresher
} else {
shopTable.addSubview(refresher)
}
}
}
What am I doing wrong?
requestWhenInUseAuthorization() is an asynchronous method, so your method that wraps it checkLocationAuthorizationStatus() is also async.
However, in your viewDidLoad, you call
checkLocationAuthorizationStatus()
self.locationManager.delegate = self
self.locationManager.startUpdatingLocation()
This is triggering the locationManager to start before it is authorized. Take a look here at this (somewhat old) link http://nshipster.com/core-location-in-ios-8/
Example
Be sure to conform to CLLocationManagerDelegate
override func viewDidLoad() {
super.viewDidLoad()
self.locationManager = CLLocationManager()
self.locationManager.delegate = self
if CLLocationManager.authorizationStatus() == .notDetermined {
locationManager.requestWhenInUseAuthorization()
} else if CLLocationManager.authorizationStatus() == . authorizedWhenInUse {
startTrackingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedAlways || status == .authorizedWhenInUse {
startTrackingLocation()
// ...
}
}
func startTrackingLocation() {
locationManager.startUpdatingLocation()
getLocale()
//not clear which of these methods require location
let locValue = self.locationManager.location?.coordinate
noHeight()
loadShops()
sortList()
print("\(locValue?.latitude), \(locValue?.longitude)")
}
You need to wait for the authorization response before using location services.
What you are doing now is requesting the authorization and the immediately starting location services. You need to be sure, the app is authorized before getting location.

Best way to get LocationUpdates with a good Accuracy without draining down the Battery

I've been trying to implement a LocationView Controller which constantly updates the Users Location to the Server. As i debugged the Application and while in use i noticed, that the SourceCode I provided drained my Phones Battery a lot faster than usual. Do I have to cope with this, or is there any way, without reducing the LocationRequirements, to improve the PowerUsage in the following Code?
class LocationViewController: UIViewController {
// MARK: - Outlets
#IBOutlet var mapView: MKMapView!
fileprivate var locations = [MKPointAnnotation]()
var updatesEnabled: Bool = false;
var defaultCentre: NotificationCenter = NotificationCenter.default
//- NSUserDefaults - LocationServicesControl_KEY to be set to TRUE when user has enabled location services.
let UDefaults: UserDefaults = UserDefaults.standard;
let LocationServicesControl_KEY: String = "LocationServices"
public var lastPosition: Date?;
public lazy var locationManager: CLLocationManager = {
let manager = CLLocationManager()
manager.distanceFilter = 20;
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
manager.requestAlwaysAuthorization()
return manager
}()
public var routeLine: MKPolyline = MKPolyline() //your line
public var routeLineView: MKPolylineView = MKPolylineView(); //overlay view
// MARK: - Actions
#available(iOS 9.0, *)
#IBAction func enabledChanged(_ sender: UISwitch) {
if sender.isOn {
self.UDefaults.set(true, forKey: self.LocationServicesControl_KEY)
locationManager.startUpdatingLocation()
locationManager.allowsBackgroundLocationUpdates = true
self.updatesEnabled = true;
} else {
self.UDefaults.set(false, forKey: self.LocationServicesControl_KEY)
locationManager.stopUpdatingLocation()
self.updatesEnabled = false;
self.timer.invalidate()
locationManager.allowsBackgroundLocationUpdates = false
}
}
#IBAction func accuracyChanged(_ sender: UISegmentedControl) {
let accuracyValues = [
kCLLocationAccuracyBestForNavigation,
kCLLocationAccuracyBest,
kCLLocationAccuracyNearestTenMeters,
kCLLocationAccuracyHundredMeters,
kCLLocationAccuracyKilometer,
kCLLocationAccuracyThreeKilometers]
locationManager.desiredAccuracy = accuracyValues[sender.selectedSegmentIndex];
}
// MARK: - Override UIViewController
override func viewDidLoad() {
mapView.delegate = self;
}
}
// MARK: - MKMapViewDelegate
extension LocationViewController: MKMapViewDelegate {
func addRoute() {
mapView.remove(self.routeLine);
var pointsToUse: [CLLocationCoordinate2D] = [];
for i in locations {
pointsToUse += [i.coordinate];
}
self.routeLine = MKPolyline(coordinates: &pointsToUse, count: pointsToUse.count)
mapView.add(self.routeLine)
}
}
// MARK: - CLLocationManagerDelegate
extension LocationViewController: CLLocationManagerDelegate {
func isUpdateValid (newDate: Date) -> Bool{
var interval = TimeInterval()
if(lastPosition==nil){lastPosition=newDate}
interval = newDate.timeIntervalSince(lastPosition!)
if ((interval==0)||(interval>=self.UpdatesInterval)){
return true
} else {
return false
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let mostRecentLocation = locations.last else {
return
}
if(isUpdateValid(newDate: mostRecentLocation.timestamp)){
let position = MKPointAnnotation();
position.coordinate=mostRecentLocation.coordinate;
self.locations.append(position);
self.addRoute();
self.locationAPI.postLocation(location: mostRecentLocation)
lastPosition=mostRecentLocation.timestamp
}
}
}
You need to limit the time when you have the GPS "lit" somehow. If you've got it set to give high accuracy GPS readings constantly, it's going to keep the GPS powered up and you're going to use a lot of power. There's nothing to be done about that.
Possibilities:
You could wake up the GPS once every 10 minutes, run it until you get a good reading, then turn it back off.
You get a good reading when your app first starts, then subscribe to significant location changes, and when you get an update, start location updates, get a new fix, and then turn off location updates again.
I'm not sure how you'd do the first option since Apple doesn't let your app run indefinitely in the background. You'd need your app to be awake and running in the foreground the whole time, which also drains the battery a lot faster than if it is allowed to go to sleep.

Resources