I have a button that uses the user's location to search around them. If they have not granted/denied permission to use their location yet, it requests the permission
Relevant code:
func updateLocationStatus(){
locationEnabled = CLLocationManager.locationServicesEnabled()
appLocationEnabled = CLLocationManager.authorizationStatus()
}
#IBAction func searchNearby(sender: UIButton) {
updateLocationStatus()
if appLocationEnabled == CLAuthorizationStatus.Denied {
//Show an alert to let them know they've denied location permissions
presentViewController(alert, animated: true, completion: nil)
}
else if appLocationEnabled == CLAuthorizationStatus.NotDetermined ||
appLocationEnabled != CLAuthorizationStatus.AuthorizedWhenInUse &&
appLocationEnabled != CLAuthorizationStatus.AuthorizedAlways{
locationManager.requestWhenInUseAuthorization()
}
else{
activityIndicator.startAnimating()
locationManager.requestLocation()
}
}
It works fine when used normally. However, if you let the "Allow app to use your location [Don't Allow] [Allow]" alert sit there for about 5 seconds, the app crashes. The line jumped to in Xcode causing the error is
locationManager.requestWhenInUseAuthorization()
and the error provided is
Thread 1: EXC_BAD_ACCESS (code=2, address=0xsomeaddress)
I don't even know where to begin to fix this because the error tells me nothing and the functionality works perfectly fine, except for when you do nothing too long
Does the backtrace provide any helpful information? In the console, you can type bt at the prompt when Xcode breaks. Alternately, you can take a look at the debug navigator.
Perhaps this will solve this question. Make sure to add the appropriate key-value combo in your Info.plist.
Kind of like this:
Also you might want to set a break point to make sure that location definitely has been enabled.
To start finding the location, you should also configure the desiredAccuracy and distanceFilter properties of the location manager and then call the requestLocation() or startUpdatingLocation().
Related
When requesting user's iOS location permissions, how could I know if locationManager.requestAlwaysAuthorization() has already been asked to the user?
In case the user had .AuthorizedWhenInUse status and the request for always authorization has been denied, the always-auth prompt for the next request won't be shown so I won't get any callback of this request launch.
Any ideas?
You need to check CLLocationManager.authorizationStatus() and only request authorization if the value is .notDetermined, since this is the only case when the authorization prompt will actually be shown.
You can check the Authorization status and compare if it's notDetermined it has not been asked, else - it's been asked.
You can know by using authorizationStatus() like this.
if CLLocationManager.authorizationStatus() == .notDetermined {
print("Not Determined")
}
else if CLLocationManager.authorizationStatus() == .restricted {
print("Restricted")
}
else if CLLocationManager.authorizationStatus() == .denied {
print("Denied")
}
else if CLLocationManager.authorizationStatus() == .authorizedAlways {
print("Always Authorized")
}
else if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
print("Authorized When Require")
}
If the Dialog appear for 1st time it returns .notDetermined status and if you respond to dialog than it returns status based on your selection like if you allow to access your location always that it returns .authorizedAlways.
After a lot of brainstorming this issue, I've found a solution.
Check authorization status each time the app starts, or when the app enters the foreground after the user views the Settings app.
If status is notDetermined, request authorizedWhenInUse. You can't jump straight to authorizedAlways.
If status is authorizedWhenInUse, request authorizedAlways.
The catch, as you know, is when the user goes from notDetermined to notDetermined, i.e. doesn't change their settings. We need to be able to see what the status was before they were prompted.
This is actually easy, just save the value of CLLocationManager.authorizationStatus() in a property called previousAuthStatus.
private var previousAuthStatus = CLLocationManager.authorizationStatus()
When the application enters the foreground, check the authorization state with that previous state.
Full code
Don't miss the last bit where previousAuthStatus is set
func checkStatus(_ status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
// request "When in use"
locationManager.requestWhenInUseAuthorization()
case .authorizedWhenInUse:
// already tried requesting "Always"?
guard previousAuthStatus != .authorizedWhenInUse else {
<#notify the user...#>
break
}
// otherwise, request "Always"
locationManager.requestAlwaysAuthorization()
case .authorizedAlways:
// start updating location
<#dismiss any warning you may have displayed#>
locationManager.startUpdatingLocation()
case .denied:
<#notify the user...#>
case .restricted:
<#notify the user...#>
#unknown default:
break
}
// save status for next check
previousAuthStatus = status
}
I'm facing the same problem. This is not a problem for app updates (where you did not store authStatus in UserDefaults in previous version) only. It is also a problem if a user decides to uninstall and reinstall the app (user defaults are removed), or user manually change the status from 'Always' to 'When in Use' (no way of knowing that it was changed to 'When in Use' or if it was that status all along).
Btw: the user manually changing the authorisation is actually very common, because iOS now brings up an alert every once in a while "AppName has been using your location in the background... do you want to continue allowing this" (or something like that don't remember exactly). Lots of my users choose 'No' in that alert, resulting in the 'Always' location access being changed to 'When in Use' without the app getting a notification about that.
iOS however does remember if requestAlwaysAccess was already asked before, no matter what. Even after an uninstall and reinstall of the app.
So, with lack of other options I'm now using this, which honestly, is not the most user friendly, but it does work and is user friendly 'enough' (for me at least).
In stead of requesting always authorisation, I simply bring up an alert, with one of the buttons pointing to the apps Settings page, where user can then manually change the setting. I did add a userdefault storing if the app showed this alert before. If it did, I won't show it again.
switch CLLocationManager.authorizationStatus() {
case .restricted, .denied:
// show an alert with one button opening settings (see below)
case .authorizedAlways:
// already have always permission, continue with your code
case .notDetermined:
// request whenInUse authorisation (you can request always authorisation here too, but iOS won't show 'always' as a choice the first time)
case .authorizedWhenInUse:
guard !UserDefaults.showedNoAlwaysAuthorisationAlert else {
return
}
UserDefaults.showedNoAlwaysAuthorisationAlert = true
// show the alert with "no thanks" and "settings" button
// button action:
if 'settingsButtonTapped', let settingsUrl = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(settingsUrl) {
UIApplication.shared.open(settingsUrl, completionHandler: nil)
}
}
I have an app which does periodic location updates to map the users path.
I am using deferred updates.
if (CLLocationManager.deferredLocationUpdatesAvailable() == true && _isDeferingUpdates == false)
{
print("Doing refresh")
_isDeferingUpdates = true
_locationManager.allowDeferredLocationUpdatesUntilTraveled(C_GPS.ACTIVITY_UPDATE_DISTANCE, timeout: C_GPS.ACTIVITY_UPDATE_TIME)
} else
{
print("Could not refresh")
// iPhone 4S does not have deferring so must keep it always on
_locationManager.startUpdatingLocation()
}
When the app is open I get the "doing refresh" call every second.
My setup:
Have the 2 keys on my pList NSLocationWhenInUseUsageDescription && NSLocationAlwaysUsageDescription
Have background modes turned on, for Remote Notifcations and Location Updates.
Have Maps setup to use Bike and Pedestrian.
Have all the permissions on my phone set to yes.
Do you know any other reason why my deferred update is failing when my app goes to the background?
Its never working and apples documentation is less than helpful
DeferredFailed The location manager did not enter deferred mode for an
unknown reason. This error can occur if GPS is unavailable, not
active, or is temporarily interrupted. If you get this error on a
device that has GPS hardware, the solution is to try again.
Here is my error handler:
func locationManager(manager: CLLocationManager, didFinishDeferredUpdatesWithError error: NSError?)
{
print("FINISHED BACKGROUND UPDATE with error \(error)")
if (error != nil)
{
print("ERROR IS VALID as CLError")
if (error!.code == CLError.LocationUnknown.rawValue)
{
print("Error: Location Unknown")
} else if (error!.code == CLError.DeferredAccuracyTooLow.rawValue)
{
print("Error: Accuracy too low")
} else if (error!.code == CLError.DeferredFailed.rawValue)
{
print("Error: Deferring Failed")
} else if (error!.code == CLError.Denied.rawValue)
{
print("Error: Denied")
} else
{
print("Error not handled")
}
}
_isDeferingUpdates = false
}
iOS 9.2, Xcode 7.2, ARC enabled
The problem is most likely associated with the distance and time interval you have chosen for your deferral. Try to take advantage of CLLocationDistanceMaxand CLTimeIntervalMax to troubleshoot the function call. For example, set the distance to CLLocationDistanceMax and then vary the time interval, then try vice versa.
But there are other things I see that you might want to change...
Get rid of CLLocationManager.deferredLocationUpdatesAvailable() == true condition in the if statements to allow deferred location updates.
Deferred updates were available iOS6.0+, most iPhone 4S can be updated to iOS7.2.1 depending on the hardware. You do not need to separately call _locationManager.startUpdatingLocation().
Make sure that if you are testing in iOS9.0+ that you have the allowsBackgroundLocationUpdates set for the location manager.
Make sure that the initial _locationManager.startUpdatingLocation() is made in the - (void)applicationWillResignActive:(UIApplication *)application method equivalent in Swift and NOT in the - (void)applicationDidEnterBackground:(UIApplication *)application method.
Make sure that you are calling _locationManager.allowDeferredLocationUpdatesUntilTraveled(C_GPS.ACTIVITY_UPDATE_DISTANCE, timeout: C_GPS.ACTIVITY_UPDATE_TIME) in the equivalent of the - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations method for Swift.
Also, note that you are printing the error before you check if the error exists, this might lead to some incorrect error reporting.
Hope this helps and I am not too late! Cheers.
P.S. If this doesn't work, then please post more of your code, i.e. where you are calling these functions in relation to the app. delegate *.m file.
The error was misleading. The issue is the 4S does not support background updates. As such, for the 4S I was manually refereshing the updates like so:
private func refreshUpdateDeferral()
{
if (CLLocationManager.deferredLocationUpdatesAvailable() == false && _isDeferingUpdates == false)
{
print("Doing deferred referral refresh")
_isDeferingUpdates = true
_locationManager.allowDeferredLocationUpdatesUntilTraveled(C_GPS.ACTIVITY_UPDATE_DISTANCE, timeout: C_GPS.ACTIVITY_UPDATE_TIME)
}
}
The problem was I had CLLocationManager.deferredLocationUpdatesAvailable() == true which mean I was deferring updates when deferring was already allowed. This seems to cause the error above.
So if you get this error, check that you aren't allowDeferredLocationUpdatesUntilTraveled while deferredUpdates is already active!
You can run into trouble with deferred updates if your accuracy and distance filter are not set correctly. Assuming your device supports deferred updates (CLLocationManager.deferredLocationUpdatesAvailable), you must set your location manager to the following first:
desiredAccuracy = kCLLocationAccuracyBest
distanceFilter = kCLDistanceFilterNone
Then you can start the location manager and allow deferred updates.
I currently have a test application to teach myself Core Location. It is an app with a button which will either show your location when you press a button, or an alert when location services are off.
I get a fatal error (unexpectedly found nil when unwrapping option value) When location services are on and I attempt to println the string. I cannot find anywhere in the documentation where the CLLocationManager.location.coordinate returns an optional value. I will include the code snippet. Thanks!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
if CLLocationManager.authorizationStatus() == .NotDetermined {
locationManager.requestWhenInUseAuthorization()
}
}
#IBAction func testLocation(sender: AnyObject) {
switch CLLocationManager.authorizationStatus() {
case .AuthorizedAlways, .AuthorizedWhenInUse:
printCurrentCoordinates()
case .Restricted, .Denied:
disabledLocationSeriveAlert()
default:
println("Failed")
}
func printCurrentCoordinates() {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
println("\(locationManager.location.coordinate.latitude)")
println("\(locationManager.location.coordinate.longitude)")
}
func disabledLocationSeriveAlert() {
let alert = UIAlertController(title: "Unable to retrieve location", message: "Enable location services in system settings", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
It looks like you aren't giving the CLLocationManager enough time to update the location.
I looked at the documentation, and in the statement locationManager.location.coordinate.latitude, location is the only one that's an optional. It's implicitly unwrapped, so that's why you don't need an ! after it.
In the printCurrentCoordinates method, you attempt to print the coordinates immediately after you call startUpdatingLocation. The documentation for location says:
The value of this property is nil if no location data has ever been retrieved.
And the documentation for startUpdatingLocation says:
This method returns immediately. Calling this method causes the location manager to obtain an initial location fix (which may take several seconds) and notify your delegate by calling its locationManager:didUpdateLocations: method.
So, you aren't giving the location manager enough time to get a location fix before you attempt to print the location.
You have a couple of choices - you can call startUpdatingLocation earlier and then print the coordinates in printCurrentCoordinates, or you could have printCurrentCoordinates start updating the location and then print the coordinates in locationManager:didUpdateLocations:.
The issue is location property of locationManager will be nil at that time.
That property contains the most recently retrieved location data. You just allocated location manager, at that time there won't be any data in that property.
According to CLLocationManager Class Reference:
location
Property The most recently retrieved user location.
(read-only)
Declaration
#NSCopying var location: CLLocation! { get }
Discussion
The value of this property is nil if no location data has
ever been retrieved.
location is nil when not set (from doc)
The value of this property is nil if no location data has ever been retrieved.
You have to use delegate to get the location. This is asynchronous operation, so value not available right after call startUpdatingLocation()
You must implement CLLocationManagerDelegate (and set your object as delegate) and wait for locationManager:didUpdateLocations: method - only then location will have some data.
Obtaining the location is an asynchronous operation - in fact the method you invoke to start the op is startUpdatingLocation() and not getLocation(). That is an indication that the process is started, and it's not completed at the time the invoked method returns.
The location is provided asynchronously via the func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) method, part of the CLLocationManagerDelegate protocol that you must adopt in your class. And of course you have to set the delegate property of the CLLocationManager instance. If you have ever worked with tables, you should know how it works.
However, I recommend reading the official docs, or a good tutorial like this one
Here is my code from a ViewController implementing CLLocationManagerDelegate:
func startLocationManager() {
let locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
println("I'm called")
locationManager.requestAlwaysAuthorization()
// locationManager.startMonitoringSignificantLocationChanges()
locationManager.startUpdatingLocation()
let status = CLLocationManager.authorizationStatus()
println(status.rawValue) // This print 0 which stands for kCLAuthorizationStatusNotDetermined
println(CLLocationManager.locationServicesEnabled()) // true
}
func locationManager(manager: CLLocationManager!,
didUpdateLocations locations: [AnyObject]!) {
println("nobody call me ever, and I'm sad")
}
For some reason, I never get the prompt / alter to autorise location updates. I have tried on my device iOS 8.1 and the simulartor. I followed the advices found here: requestAlwaysAuthorization not showing permission alert :
"Add Core Location framework to Project Settings / Targets / Capabilities / Background Modes set "Location Updates" and "Uses Bluetooth LE Accessories" Add key at Info.plist NSLocationAlwaysUsageDescription".
I have also tried to clean up and rebuild, nothing change. I feel clueless.
EDIT: This question seems related: iOS: App is not asking user's permission while installing the app. getting kCLAuthorizationStatusNotDetermined every time - Objective-c & Swift but the selected answer and its article doesn't expose anything new
Your CLLocationManager object is local object and thus will be deallocated immediately after it falls out of scope. Make it a class property and then asynchronous processes like requesting authorization and determining the location will have a chance to run.
problem :
For some reason, the app I’m working on is not asking for permission to use location services. This used to work before. Also, I created a new project in which I followed the steps described below EXACTLY in which this worked immediately.
One difference is I know the authorization status in the main app I’m working on is .Denied, it was .NotDetermined in the test project.
I read this answe on stack overflow :
Core Location not requesting user permission
'
Not many people know this, but after you uninstall an application, that application's documents and preferences are still stored on the device, here:
/var/mobile/Library/Safe Harbor/myappidentifier/Container/
in my opinion, this was not a wise move by apple, as that could have security risks, as the one you have explained above.
If an app is re-installed, iOS automatically copies those preferences back into the appropriate folder. That is the cause of the behavior you are seeing.
'
This could explain it because I did reinstall the app but it doesn't say how to solve it.
Thank you
Here’s the relevant code i wrote in the App :
I set the NSLocationAlwaysUsageDescription key in info.plist
I imported core location :
import CoreLocation
implemented the right protocol:
CLLocationManagerDelegate
set this property :
var locationManager = CLLocationManager()
set the delegate in ViewDidLoad
locationManager.delegate = self
and wrote a test function to test this :
if CLLocationManager.locationServicesEnabled() {
println("Location Services Enabled")
if CLLocationManager.authorizationStatus() == .Authorized {
println("Location Services Authorized")
} else if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {
println("Location Services Authorized WHEN IN USE")
} else if CLLocationManager.authorizationStatus() == .NotDetermined {
println("Location Services NOT Authorized: NOT DETERMINED")
} else if CLLocationManager.authorizationStatus() == .Restricted {
println("Location Services NOT Authorized: RESTRICTED")
} else if CLLocationManager.authorizationStatus() == .Denied {
println("Location Services NOT Authorized: DENIED")
} else {
println("Location Services NOT Authorized")
locationManager.requestAlwaysAuthorization()
}
} else {
println("!!! Location Services NOT Enabled !!!")
}
Console Output :
Location Services Enabled
Location Services NOT Authorized
You can go into the Settings app, General, and reset Location & Privacy to get the initial permission dialog for your app again. Otherwise, iOS saves the first answer without ever asking again.