36

In my app I have a tab called "Discover". The Discover tab will use the users current location to find "stuff" near them. Instead of presenting the user with a generic Authorization Request (which typically gets declined, based on research), I present the user with a modal explaining what we're asking for. If they say yes, THEN the actual Authorization message pops up.

However, the user still has the option to say no the prompt. Is there a way to add a callback to the prompt so that once the user selects an option I can see if they accepted or declined?

I have tried this:

func promptForLocationDataAccess() {
    locationManager.requestWhenInUseAuthorization()
    println("test")
}

As expected, the "println" executes at the same time that the request prompt comes up, so I can't do it that way.

The problem is that if the user opts to not use Location Data, the content served will be different than if they had accepted.

Ideally I'm hoping for a callback of some kind, but I'll take any logical direction at this point!

matcartmill
  • 1,573
  • 2
  • 19
  • 38

7 Answers7

62

You can use the locationManager:didChangeAuthorizationStatus: CLLocationManagerDelegate method as a "callback" of sorts.

- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
    if (status == kCLAuthorizationStatusDenied) {
        // The user denied authorization
    }
    else if (status == kCLAuthorizationStatusAuthorized) {
        // The user accepted authorization
    }
}

And in Swift (update suggested by user Michael Marvick, but rejected for some reason...):

func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
    if (status == CLAuthorizationStatus.denied) {
        // The user denied authorization
    } else if (status == CLAuthorizationStatus.authorizedAlways) {
        // The user accepted authorization
    } 
}
James Toomey
  • 5,635
  • 3
  • 37
  • 41
Lyndsey Scott
  • 37,080
  • 10
  • 92
  • 128
  • Thanks, I tried that but the method would never fire. I did a simple println("test") without any if statements and it wouldn't work. I'm currently working on trying to integrate an NSNotificationCentre bit, as it seems that when an alert is displayed, the application becomes inactive. Once the alert is dismissed, the app delegate method applicationDidBecomeActive fires. I will post back on if I get this working. – matcartmill Jan 15 '15 at 00:30
  • 1
    @matcartmill Have you included CLLocationManagerDelegate within the class and set the CLLocationManager's delegate to self? – Lyndsey Scott Jan 15 '15 at 00:31
  • Yes I have. I set the locationManager.delegate to self in the viewDidLoad area. This my delegate function for the LocationManager func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus!) { println("Changed") } It doesn't print anything. – matcartmill Jan 15 '15 at 00:46
  • Oops! I realized that I added a ! to my function. Removed it and now it's working. Much better than adding a Notification Center feature. – matcartmill Jan 15 '15 at 00:51
  • 1
    kCLAuthorizationStatusAuthorized is deprecated, use kCLAuthorizationStatusAuthorizedAlways or kCLAuthorizationStatusAuthorizedWhenInUse. – Besfort Abazi Mar 23 '19 at 22:03
  • @LyndseyScott I had an SDK for location tracking, in iOS 13 when I'm checking "CLAuthorizationStatus" it's returning 3 even it has set to "while using the App" so "Always" and "while using the App" have same result!!! Do you have any idea about this wired behavior? – Mo Farhand Nov 22 '19 at 02:56
7

When the authorisation status for location changes, the delegate method didChangeAuthorizationStatus: will be called.

When you call requestWhenInUseAuthorization the first time after your app is installed the delegate method will be called with status kCLAuthorizationStatusNotDetermined (0).

If the user declines location services access then the delegate method will be called again with status kCLAuthorizationStatusDenied (2).

If the user approves location services access then the delegate method will be called again with status kCLAuthorizationStatusAuthorizedAlways (3) or kCLAuthorizationStatusAuthorizedWhenInUse (4) depending on the permission that was requested.

On subsequent executions of your app the delegate method will receive status kCLAuthorizationStatusDenied or kCLAuthorizationStatusAuthorizedAlways/kCLAuthorizationStatusAuthorizedWhenInUse after calling requestWhenInUseAuthorization based on the current state of location services permission for the app in the device settings.

Paulw11
  • 108,386
  • 14
  • 159
  • 186
6

Swift 3

func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
    if (status == CLAuthorizationStatus.denied) {
        // The user denied authorization
    } else if (status == CLAuthorizationStatus.authorizedAlways) {
        // The user accepted authorization
    } 
}
user2213459
  • 111
  • 1
  • 3
5

This solution isn't the best in all scenarios, but it worked for me so I thought I'd share:

import Foundation
import CoreLocation

class LocationManager: NSObject, CLLocationManagerDelegate {
    static let sharedInstance = LocationManager()
    private var locationManager = CLLocationManager()
    private let operationQueue = OperationQueue()

    override init(){
        super.init()

        //Pause the operation queue because
        // we don't know if we have location permissions yet
        operationQueue.isSuspended = true
        locationManager.delegate = self
    }

    ///When the user presses the allow/don't allow buttons on the popup dialogue
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {

        //If we're authorized to use location services, run all operations in the queue
        // otherwise if we were denied access, cancel the operations
        if(status == .authorizedAlways || status == .authorizedWhenInUse){
            self.operationQueue.isSuspended = false
        }else if(status == .denied){
            self.operationQueue.cancelAllOperations()
        }
    }

    ///Checks the status of the location permission
    /// and adds the callback block to the queue to run when finished checking
    /// NOTE: Anything done in the UI should be enclosed in `DispatchQueue.main.async {}`
    func runLocationBlock(callback: @escaping () -> ()){

        //Get the current authorization status
        let authState = CLLocationManager.authorizationStatus()

        //If we have permissions, start executing the commands immediately
        // otherwise request permission
        if(authState == .authorizedAlways || authState == .authorizedWhenInUse){
            self.operationQueue.isSuspended = false
        }else{
            //Request permission
            locationManager.requestAlwaysAuthorization()
        }

        //Create a closure with the callback function so we can add it to the operationQueue
        let block = { callback() }

        //Add block to the queue to be executed asynchronously
        self.operationQueue.addOperation(block)
    }
}

Now every time you want to use location information, just surround it with this:

LocationManager.sharedInstance.runLocationBlock {
    //insert location code here
}

So whenever you try using the location information, the authorization status is checked. If you don't have permission yet, it requests permission and waits until the user presses the "Allow" button or the "Don't allow" button. If the "Allow" button is pressed, any requests for location data will be processed on separate threads, but if the "Don't Allow" button is pressed, all location requests will be canceled.

tdon
  • 1,421
  • 15
  • 24
3

Working code in Swift 5:

Plist: Add the entry

<key>NSLocationWhenInUseUsageDescription</key>
  <string>Needs Location when in use</string>


import UIKit
import CoreLocation

class ViewController: UIViewController {
    var locationManager: CLLocationManager?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        locationManager = CLLocationManager()
        
        //Make sure to set the delegate, to get the call back when the user taps Allow option
        locationManager?.delegate = self
    }
}

extension ViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .notDetermined:
            print("not determined - hence ask for Permission")
            manager.requestWhenInUseAuthorization()
        case .restricted, .denied:
            print("permission denied")
        case .authorizedAlways, .authorizedWhenInUse:
            print("Apple delegate gives the call back here once user taps Allow option, Make sure delegate is set to self")
        }
    }
}
Naishta
  • 11,885
  • 4
  • 72
  • 54
1

Objective C

For a block callback on didChangeAuthorizationStatus add this in .h

@property void(^authorizationCompletionBlock)(BOOL);

and following in .m

-(void)locationManager:(CLLocationManager *)locationManager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
_authorizationStatus = status;

switch (status) {
    case kCLAuthorizationStatusAuthorizedAlways:
    case kCLAuthorizationStatusAuthorizedWhenInUse:
        if (self.authorizationCompletionBlock) {
            self.authorizationCompletionBlock(YES); // this fires block
        }
    default:
        if (self.authorizationCompletionBlock) {
            self.authorizationCompletionBlock(NO); // this fires block
        }
        break;
    }
}

and add handler like this:

// this listens block
// in your VC or Utility class
authorizationCompletionBlock = ^(BOOL isGranted) {
    completionBlock(isGranted);
};

Swift 3.2

var authorizationCompletionBlock:((Bool)->())? = {_ in}


func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
    switch (status)
    {
    case (.authorizedWhenInUse):
        if authorizationCompletionBlock != nil
        {
            authorizationCompletionBlock!(true)
        }

    default:
        if authorizationCompletionBlock != nil
        {
            authorizationCompletionBlock!(false);
        }
    }
}

and handler like this

authorizationCompletionBlock = { isGranted in
        print(isGranted)
    }
Muhammad Waqas
  • 904
  • 2
  • 10
  • 21
0

Similar to tdon's response above, I created a function with a completion block to communicate the status once it is retrieved from the device:

func retrieveAuthorizationStatus(completion: @escaping (TrackingState) -> ()) {
    let status = CLLocationManager.authorizationStatus()
    switch status {
    case .authorizedWhenInUse:
        completion(.off)
    default:
        completion(.issue)
    }
}

TrackingState is a separate enum I'm using to manage display within a view controller. You could just as easily pass the authorizationStatus() in the completion block:

func retrieveAuthorizationStatus(completion: @escaping (CLAuthorizationStatus) -> ()) {
    let status = CLLocationManager.authorizationStatus()
    completion(status)
}
B-Rad
  • 353
  • 3
  • 11