Is this acceptable use of location authorization callback? - ios

I have implemented a location authorization callback function and it works, but looking over the code I seem to have got the function to trigger but never actually use the function as intended. Please can you comment on how bad I am being and if it is acceptable to do what I have done.
import UIKit
import CoreLocation
class ViewController___Main_menu: UIViewController, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
print("services are enabled")
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
print("Not determined")
case .restricted:
print("Resticted")
case .denied:
print("Denied")
case .authorizedAlways, .authorizedWhenInUse:
print("Access")
#unknown default:
print("unknown error")
}
} else {
print("not enabled")
}
}
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus){
print("Status: \(status)")
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
print("Not determined")
case .restricted:
print("Resticted")
case .denied:
print("Denied")
case .authorizedAlways, .authorizedWhenInUse:
print("Access")
#unknown default:
print("unknown error")
}
}
}

It's looks like you're requesting access to location services and straight after that checking what the authorization status is, expecting it to be available immediately. This isn't the way it works. When you request location with locationManager.requestWhenInUseAuthorization the request happens asynchronously.
When the location status changes then it will call to the function you're included locationManager(_:didChangeAuthorization) with the new status. This will happen when the user taps "Allow" or "Deny" on the prompt that appears. It can also happen when they go into the settings and change the permissions there
So to make it more clear, check and log the authorization status before requesting location. Use the following methods to know if the location request succeeded or failed:
func locationManager(CLLocationManager, didUpdateLocations: [CLLocation])
func locationManager(CLLocationManager, didFailWithError: Error)
You would use the CLLocationManager.locationServicesEnabled() check to wrap the request for location services. If location is turned off for the device then requesting location will just cause an error.

Related

swift - didUpdateLocations Not Being Called

I'm working on an app that requires getting the user's current coordinates. I was planning on doing this through CLLocationManager's didUpdateLocations method. For some reason, didUpdateLocations is not being executed. However, it appears that locationManager.startUpdatingLocation() is being called successfully. None of the other possible solutions I've seen on this site have worked for me. I already added NSLocationAlwaysUsage to info.plist. Here is the entirety of my code:
import UIKit
import MapKit
import CoreLocation
var region: MKCoordinateRegion!
class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {
#IBOutlet weak var map: MKMapView!
var locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
self.locationManager.requestAlwaysAuthorization()
self.locationManager.requestWhenInUseAuthorization()
switch CLLocationManager.authorizationStatus() {
case .authorizedWhenInUse, .authorizedAlways:
if CLLocationManager.locationServicesEnabled() {
locationManager.startUpdatingLocation()
print("Updating location now")
}
case .notDetermined:
locationManager.requestAlwaysAuthorization()
case .restricted, .denied:
print("User must enable access in settings")
break
}
if (region == nil){
}
else {
map.setRegion(region!, animated: true)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("Got location")
let userLocation:CLLocation = locations[0]
let lat:CLLocationDegrees = userLocation.coordinate.latitude
let long:CLLocationDegrees = userLocation.coordinate.longitude
let currentPos:CLLocationCoordinate2D = CLLocationCoordinate2DMake(lat, long)
didUpdateRegion(position: currentPos)
print(lat)
print(long)
}
func didUpdateRegion(position: CLLocationCoordinate2D) {
let span = MKCoordinateSpanMake(0.075, 0.075)
region = MKCoordinateRegion(center: position, span: span)
}
func locationManager(_manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
// If status has not yet been determied, ask for authorization
manager.requestWhenInUseAuthorization()
break
case .authorizedWhenInUse:
// If authorized when in use
manager.startUpdatingLocation()
break
case .authorizedAlways:
// If always authorized
manager.startUpdatingLocation()
break
case .restricted:
print("User must activate location services in settings")
break
case .denied:
print("User must activate location services in settings")
break
default:
break
}
}
When I run this code on both the simulator and an actual device, I get the notification to allow location tracking. After accepting that, the console displays "Updating location now," but never gets to printing "Got location." Thank you for any light you can shed on this issue, I'm new to app development in general.
EDIT: I added in the entirety of my code instead of just the parts I thought were relevant. Basically, I'm trying to get the region shown on the map to follow the user. I attempt to do this by updating the variable "region" every time the didUpdateLocations function fires.
Am I getting it right and you only added one key - NSLocationAlwaysUsage?
Try to add both keys to the Info.plist:
Privacy - Location When In Use Usage Description
Privacy - Location Always Usage Description
Also, what happens if you implement this method of protocol?
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("\(error.localizedDescription)")
}
Does it print anything? Sorry, I was going to leave a comment, but I don't have enough reputation.

Prompt user to allow location services after being denied without crashing

The problem I am facing is that when I press a UIButton - location services are required to initiate the action. However if the user was to deny Location Services at the initial launch of the app - the app will crash.
I have tried finding a way to implement CLAuthorizationStatus .Denied but I can't seem to find a way to do so. The only code I can seem to implement is the didChangeAuthorizationStatus which only initiates the request at First Launch of the application.
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus)
{
if status == .AuthorizedAlways || status == .AuthorizedWhenInUse
{
manager.startUpdatingLocation()
}
else
{
manager.requestWhenInUseAuthorization()
}
}
If I press the UIButton to send the API Request the app will crash if location services have been denied.
My question is how can I implement a method, within the button's IBAction, that will direct the user to go to their settings and enable location services. :)
CLLocationManager has a static function authorizationStatus() that you can use to get the current authorization status without even initializing a CLLocationManager object.
So in the function that you call when the user presses the button you can check the authorization status and act accordingly:
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate {
lazy var locationManager = CLLocationManager()
...
func didPressButton(sender: UIButton) {
switch CLLocationManager.authorizationStatus() {
case .AuthorizedAlways, .AuthorizedWhenInUse:
locationManager.delegate = self
locationManager.startUpdatingLocation()
case .NotDetermined:
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
case .Denied:
print("Show Alert with link to settings")
case .Restricted:
// Nothing you can do, app cannot use location services
break
}
}
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if status == .AuthorizedWhenInUse {
manager.startUpdatingLocation()
}
}
}

Notification if requestWhenInUseAuthorization failed

I'm trying to create a sort of locationServices permission authentication, where you only can use the app if it has permission to locationServices. Therefore if the user declines locationManager.requestWhenInUseAuthorization() it should present a viewController with info about this. However i can't seem to get a notification if the user declined this. What is the best solution for this? or should consider to achieve this another way?
func determinePermission() {
switch CLLocationManager.authorizationStatus() {
case .AuthorizedAlways, .AuthorizedWhenInUse:
if CLLocationManager.locationServicesEnabled() {
}
case .NotDetermined:
locationManager.requestWhenInUseAuthorization()
case .Restricted, .Denied:
let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let vc: UIViewController = storyboard.instantiateViewControllerWithIdentifier("PermissionController") as! PermissionViewController
self.parentViewController!.tabBarController!.presentViewController(vc, animated: true, completion: nil)
}
}
Implement CLLocationManagerDelegate protocol.CLLocationManagerDelegate has locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) method which gets called when authorisation status is changed.
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
switch status {
case .AuthorizedWhenInUse:
case .AuthorizedAlways:
// you have access
break
case .Denied :
default:
// you don't have access. add your code here to present new view
break
}
}

Check if location services are enabled

I've been doing some research about CoreLocation. Recently, I encountered a problem that has been covered elsewhere, but in Objective C, and for iOS 8.
I feel kinda silly asking this, but how can you check if location services are enabled using swift, on iOS 9?
On iOS 7 (and maybe 8?) you could use locationServicesEnabled(), but that doesn't appear to be working when compiling for iOS 9.
So how would I accomplish this?
Thanks!
Add the CLLocationManagerDelegate to your class inheritance and then you can make this check:
Import CoreLocation Framework
import CoreLocation
Swift 1.x - 2.x version:
if CLLocationManager.locationServicesEnabled() {
switch CLLocationManager.authorizationStatus() {
case .NotDetermined, .Restricted, .Denied:
print("No access")
case .AuthorizedAlways, .AuthorizedWhenInUse:
print("Access")
}
} else {
print("Location services are not enabled")
}
Swift 4.x version:
if CLLocationManager.locationServicesEnabled() {
switch CLLocationManager.authorizationStatus() {
case .notDetermined, .restricted, .denied:
print("No access")
case .authorizedAlways, .authorizedWhenInUse:
print("Access")
}
} else {
print("Location services are not enabled")
}
Swift 5.1 version
if CLLocationManager.locationServicesEnabled() {
switch CLLocationManager.authorizationStatus() {
case .notDetermined, .restricted, .denied:
print("No access")
case .authorizedAlways, .authorizedWhenInUse:
print("Access")
#unknown default:
break
}
} else {
print("Location services are not enabled")
}
iOS 14.x
In iOS 14 you will get the following error message:
authorizationStatus() was deprecated in iOS 14.0
To solve this, use the following:
private let locationManager = CLLocationManager()
if CLLocationManager.locationServicesEnabled() {
switch locationManager.authorizationStatus {
case .notDetermined, .restricted, .denied:
print("No access")
case .authorizedAlways, .authorizedWhenInUse:
print("Access")
#unknown default:
break
}
} else {
print("Location services are not enabled")
}
In objective-c
you should track user already denied or not determined then ask for permission or sent user to Setting app.
-(void)askEnableLocationService
{
BOOL showAlertSetting = false;
BOOL showInitLocation = false;
if ([CLLocationManager locationServicesEnabled]) {
switch ([CLLocationManager authorizationStatus]) {
case kCLAuthorizationStatusDenied:
showAlertSetting = true;
NSLog(#"HH: kCLAuthorizationStatusDenied");
break;
case kCLAuthorizationStatusRestricted:
showAlertSetting = true;
NSLog(#"HH: kCLAuthorizationStatusRestricted");
break;
case kCLAuthorizationStatusAuthorizedAlways:
showInitLocation = true;
NSLog(#"HH: kCLAuthorizationStatusAuthorizedAlways");
break;
case kCLAuthorizationStatusAuthorizedWhenInUse:
showInitLocation = true;
NSLog(#"HH: kCLAuthorizationStatusAuthorizedWhenInUse");
break;
case kCLAuthorizationStatusNotDetermined:
showInitLocation = true;
NSLog(#"HH: kCLAuthorizationStatusNotDetermined");
break;
default:
break;
}
} else {
showAlertSetting = true;
NSLog(#"HH: locationServicesDisabled");
}
if (showAlertSetting) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:#"Please enable location service for this app in ALLOW LOCATION ACCESS: Always, Go to Setting?" delegate:self cancelButtonTitle:#"No" otherButtonTitles:#"Open Setting", nil];
alertView.tag = 199;
[alertView show];
}
if (showInitLocation) {
[self initLocationManager];
}
}
Implement alertView Delegate then sent user to enable location service if already deny by user.
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (alertView.tag == 199) {
if (buttonIndex == 1) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
}
return;
}
}
Init Location Manager
-(void)initLocationManager{
self.locationManager = [[CLLocationManager alloc] init];
if([self.locationManager respondsToSelector:#selector(requestAlwaysAuthorization)]) {
[self.locationManager requestAlwaysAuthorization];
}
}
Please note kCLAuthorizationStatusAuthorizedAlways and kCLAuthorizationStatusAuthorizedWhenInUse is difference.
Here is the format Apple recommends.
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
// Request when-in-use authorization initially
break
case .restricted, .denied:
// Disable location features
break
case .authorizedWhenInUse, .authorizedAlways:
// Enable location features
break
}
Here is a complete example.
This includes an AlertView with a button to take the user to the Settings screen if previously denied access.
import CoreLocation
let locationManager = CLLocationManager()
class SettingsTableViewController:CLLocationManagerDelegate{
func checkUsersLocationServicesAuthorization(){
/// Check if user has authorized Total Plus to use Location Services
if CLLocationManager.locationServicesEnabled() {
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
// Request when-in-use authorization initially
// This is the first and the ONLY time you will be able to ask the user for permission
self.locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
break
case .restricted, .denied:
// Disable location features
switchAutoTaxDetection.isOn = false
let alert = UIAlertController(title: "Allow Location Access", message: "MyApp needs access to your location. Turn on Location Services in your device settings.", preferredStyle: UIAlertController.Style.alert)
// Button to Open Settings
alert.addAction(UIAlertAction(title: "Settings", style: UIAlertAction.Style.default, handler: { action in
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
return
}
if UIApplication.shared.canOpenURL(settingsUrl) {
UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
print("Settings opened: \(success)")
})
}
}))
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertAction.Style.default, handler: nil))
self.present(alert, animated: true, completion: nil)
break
case .authorizedWhenInUse, .authorizedAlways:
// Enable features that require location services here.
print("Full Access")
break
}
}
}
}
SWIFT (As of July 24, 2018)
if CLLocationManager.locationServicesEnabled() {
}
this will tell you if the user has already selected a setting for the app's location permission request
It is just a 2 line function in Swift 4:
import CoreLocation
static func isLocationPermissionGranted() -> Bool
{
guard CLLocationManager.locationServicesEnabled() else { return false }
return [.authorizedAlways, .authorizedWhenInUse].contains(CLLocationManager.authorizationStatus())
}
For swift3.0 and above ,
if frequent checks are made for the availability of location services, create a class like below,
import CoreLocation
open class Reachability {
class func isLocationServiceEnabled() -> Bool {
if CLLocationManager.locationServicesEnabled() {
switch(CLLocationManager.authorizationStatus()) {
case .notDetermined, .restricted, .denied:
return false
case .authorizedAlways, .authorizedWhenInUse:
return true
default:
print("Something wrong with Location services")
return false
}
} else {
print("Location services are not enabled")
return false
}
}
}
and then use it like this in your VC
if Reachability.isLocationServiceEnabled() == true {
// Do what you want to do.
} else {
//You could show an alert like this.
let alertController = UIAlertController(title: "Location
Services Disabled", message: "Please enable location services
for this app.", preferredStyle: .alert)
let OKAction = UIAlertAction(title: "OK", style: .default,
handler: nil)
alertController.addAction(OKAction)
OperationQueue.main.addOperation {
self.present(alertController, animated: true,
completion:nil)
}
}
When you call -startLocation, if location services were denied by the user, the location manager delegate will receive a call to - locationManager:didFailWithError: with the kCLErrorDenied error code. This works both in all versions of iOS.
In Swift 3.0
if (CLLocationManager.locationServicesEnabled())
{
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
if ((UIDevice.current.systemVersion as NSString).floatValue >= 8)
{
locationManager.requestWhenInUseAuthorization()
}
locationManager.startUpdatingLocation()
}
else
{
#if debug
println("Location services are not enabled");
#endif
}
To ask for permission for location services you use:
yourSharedLocationManager.requestWhenInUseAuthorization()
If the status is currently undetermined an alert will show prompting the user to allow access. If access is denied your app will be notified in the CLLocationManagerDelegate, likewise if permission is denied at any point you will be updated here.
There are two separate statuses you need to check to determine the current permissions.
If the user has the general location services enabled or not
CLLocationManager.locationServicesEnabled()
If the user has granted the correct permission for your app..
CLLocationManager.authorizationStatus() == .authorizedWhenInUse
You could add an extension is a handy option:
extension CLLocationManager {
static func authorizedToRequestLocation() -> Bool {
return CLLocationManager.locationServicesEnabled() &&
(CLLocationManager.authorizationStatus() == .authorizedAlways || CLLocationManager.authorizationStatus() == .authorizedWhenInUse)
}
}
Here it is being accessed when the user has first requested directions:
private func requestUserLocation() {
//when status is not determined this method runs to request location access
locationManager.requestWhenInUseAuthorization()
if CLLocationManager.authorizedToRequestLocation() {
//have accuracy set to best for navigation - accuracy is not guaranteed it 'does it's best'
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
//find out current location, using this one time request location will start the location services and then stop once have the location within the desired accuracy -
locationManager.requestLocation()
} else {
//show alert for no location permission
showAlertNoLocation(locationError: .invalidPermissions)
}
}
Here is the delegate:
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if !CLLocationManager.authorizedToRequestLocation() {
showAlertNoLocation(locationError: .invalidPermissions)
}
}
Swift 5.2
First, set up User class as a CLLocationManager delegate:
import SwiftUI
import CoreLocation
class User: NSObject, ObservableObject {
let manager = CLLocationManager()
override init() {
super.init()
manager.delegate = self
manager.requestWhenInUseAuthorization()
manager.requestLocation()
manager.startUpdatingLocation()
}
}
extension User: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
print("Location services authorization request")
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("Location updated")
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Failed to find user's location: \(error.localizedDescription)")
}
}
Then in a view:
if (user.manager.authorizationStatus == .denied) {
print("Location authorization denied, displaying sheet.")
}
Here is my solution:
import CoreLocation
import Combine
class LocationManager: NSObject, CLLocationManagerDelegate {
static let shared = LocationManager()
private (set) var didChangeLocationAuthorization: CurrentValueSubject<CLAuthorizationStatus, Never> = .init(.notDetermined)
private let manager = CLLocationManager()
private let notificationCenter = NotificationCenter.default
var authorizationStatus: CLAuthorizationStatus = .notDetermined
private override init() { }
func checkLocationService() {
setupLocationManager()
checkLocationManagerAuthorization()
}
private func setupLocationManager() {
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
}
private func checkLocationManagerAuthorization() {
authorizationStatus = manager.authorizationStatus
switch authorizationStatus{
case .notDetermined:
print("::: -> Location: notDetermined")
manager.requestWhenInUseAuthorization()
case .authorizedAlways, .authorizedWhenInUse:
print("::: -> Location: authorizedWhenInUse")
manager.startUpdatingLocation()
case .denied, .restricted:
print("::: -> Location: denied")
default:
break
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
checkLocationManagerAuthorization()
didChangeLocationAuthorization.send(manager.authorizationStatus)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
manager.stopUpdatingLocation()
}
}
Use like this:
LocationManager.shared.checkLocationService()
LocationManager.shared.didChangeLocationAuthorization
.sink { [weak self] authorization in
print("::: Location Permission: \(authorization)")
}.store(in: &cancelBag)

Swift LocationManager didChangeAuthorizationStatus Always Called

I have a view controller which implements the CLLocationManagerDelegate. I create a the CLLocationManager variable:
let locationManager = CLLocationManager()
Then in the viewDidLoad, I set properties:
// Set location manager properties
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.distanceFilter = 50
The problem comes that the function gets called even before I check the authorization status.
func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if (status == .AuthorizedWhenInUse) {
// User has granted autorization to location, get location
locationManager.startUpdatingLocation()
}
}
Can anyone inform me what could be causing this to occur?
- locationManager:didChangeAuthorizationStatus: is called shortly after the CLLocationManager is initialised.
You can request authorization inside the delegate method if you want:
func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
locationManager.requestAlwaysAuthorization()
break
case .authorizedWhenInUse:
locationManager.startUpdatingLocation()
break
case .authorizedAlways:
locationManager.startUpdatingLocation()
break
case .restricted:
// restricted by e.g. parental controls. User can't enable Location Services
break
case .denied:
// user denied your app access to Location Services, but can grant access from Settings.app
break
default:
break
}
}
Be aware that you need to assign the delegate in a 'timely' matter if you want this to work.
If you would somehow delay the delegate assignment, e.g. by setting it asynchronously, you might miss the initial call to - locationManager:didChangeAuthorizationStatus:.
Swift 3
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
manager.requestAlwaysAuthorization()
break
case .authorizedWhenInUse:
manager.startUpdatingLocation()
break
case .authorizedAlways:
manager.startUpdatingLocation()
break
case .restricted:
// restricted by e.g. parental controls. User can't enable Location Services
break
case .denied:
// user denied your app access to Location Services, but can grant access from Settings.app
break
}
}
The other answers could introduce new undesirable behaviors.
You can just add a boolean and a guard to prevent the first call, with some comments explaining the bug :
var firstTimeCalled = true
// ...
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
guard !firstTimeCalled else {
firstTimeCalled = false
return
}
// ... send status to listeners
}

Resources