Search code examples
iosswiftseguecllocationmanager

Double segue behavior when user authorizes location use


In my iOS application I’m requesting the user’s permission to obtain the device’s location. On my main ViewController I have a navigation bar button that when tapped, it will ask the user for permission for when in use. If the user taps OK, it will then be send to the view controller that displays the local data. If the user taps Cancel then nothing happens. I also have a pop up for when and if the user taps again on the location button to be redirected to the settings to authorize location use if previously cancelled.

The app works as I intended in the simulator but when used on a device, when the user taps OK to allow location use, it segues to the local View Controller but it does so 2 or 3 times consecutively.

The segue goes from the main view controller to the local view controller and it requests permissions from the button tap using an IBAction.

The location information is obtained in the main view controller and passed to the local controller. The local controller displays everything as it is intended.

How can I prevent this double or triple segue to the same View Controller?

Below is my code:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "toLocal" {
        let destination = segue.destination as! LocalViewController
        destination.latitude = latitude
        destination.longitude = longitude
    }
}

//MARK: - Location Manager Methods
@IBAction func LocationNavBarItemWasTapped(sender: AnyObject) {
    locationManager.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
    locationManager.requestWhenInUseAuthorization()
    locationManager.startUpdatingLocation()
}

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    let location = locations[locations.count - 1]
    if location.horizontalAccuracy > 0 {
        locationManager.stopUpdatingLocation()
        latitude = location.coordinate.latitude
        longitude = location.coordinate.longitude
    }

    let status = CLLocationManager.authorizationStatus()
    switch status {
    case .restricted, .denied:
        showLocationDisabledPopUp()
        return
    case .notDetermined:
        // Request Access
        locationManager.requestWhenInUseAuthorization()
    case .authorizedAlways:
        print("Do Nothing: authorizedAlways")
    case .authorizedWhenInUse:
        self.performSegue(withIdentifier: "toLocal", sender: nil)
    }
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    print("LocationManager failed with error \(error)")
}

private func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
    if (status == CLAuthorizationStatus.denied) {
        showLocationDisabledPopUp()
    }
}

Solution

  • As superpuccio already stated the main issue is that the didUpdateLocations delegate function is called multiple times. I also do not know why you are checking the authorizationStatus in the didUpdateLocations function since at that point it is already clear that the user allowed location access. In my opinion the function should look something like this:

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // check for a location that suits your needs
        guard let location = locations.last, location.horizontalAccuracy > 0 else { return }
    
        // prevent the manager from updating the location and sending more location events
        manager.delegate = nil
        manager.stopUpdatingLocation()
    
        // update the local variables
        latitude = location.coordinate.latitude
        longitude = location.coordinate.longitude
    
        // perform the segue
        performSegue(withIdentifier: "toLocal", sender: nil)
    }
    

    Since there are some more issues like starting location updates before knowing the actual authorization status I'll provide a full solution like I'd do it. Feel free to ask if anything is unclear:

    class ViewController: UIViewController {
    
        var latitude: CLLocationDegrees?
        var longitude: CLLocationDegrees?
    
        lazy var locationManager: CLLocationManager = {
            let locationManager = CLLocationManager()
            locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
            return locationManager
        }()
    
        @IBAction func getLocation(_ sender: UIBarButtonItem) {
            locationManager.delegate = self
            checkAuthorizationStatus()
        }
    
        private func checkAuthorizationStatus(_ status: CLAuthorizationStatus? = nil) {
            switch status ?? CLLocationManager.authorizationStatus() {
            case .notDetermined:
                locationManager.requestWhenInUseAuthorization()
            case .authorizedWhenInUse:
                locationManager.startUpdatingLocation()
            default:
                showLocationDisabledPopUp()
            }
        }
    
        func showLocationDisabledPopUp() {
            // your popup code
        }
    
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            // your segue code
        }
    
    }
    
    extension ViewController: CLLocationManagerDelegate {
    
        func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
            checkAuthorizationStatus(status)
        }
    
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            guard let location = locations.last, location.horizontalAccuracy > 0 else { return }
    
            manager.delegate = nil
            manager.stopUpdatingLocation()
    
            latitude = location.coordinate.latitude
            longitude = location.coordinate.longitude
    
            performSegue(withIdentifier: "toLocal", sender: nil)
        }
    
    }