iOS Swift 3: Location Privacy Denied - swift

What if the location Privacy Access for an app is Denied?
Evening guys, I'm coding a simple App that uses location when in use.
The Design Pattern
Before everything, when you launch the App, it will check if there is a permission already set.
If not, it shows an alert asking for permission.
If yes and granted, it proceeds doing its job.
If yes and denied, it shows an alert asking to grant access with a button pointing to settings.
it should be possible to return back from the settings to the app.
The apps does its job.
If the user change the privacy settings when the app is still open, the app should be notified, and repeat number 1.
The Code so Far
MainController
let manager = LocationManager()
override func viewDidLoad() {
super.viewDidLoad()
// ...
if manager.getPermission() == false {
//show alert
showAcessDeniedAlert()
}
manager.onLocationFix = {
//This is a function used for a closure
}
}
}
func showAcessDeniedAlert() {
let alertController = UIAlertController(title: "Location Accees Requested",
message: "The location permission was not authorized. Please enable it in Settings to continue.",
preferredStyle: .alert)
let settingsAction = UIAlertAction(title: "Settings", style: .default) { (alertAction) in
// THIS IS WHERE THE MAGIC HAPPENS!!!!
if let appSettings = URL(string: UIApplicationOpenSettingsURLString) {
UIApplication.shared.open(appSettings as URL)
}
}
alertController.addAction(settingsAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
LocationManager
import CoreLocation
extension Coordinate {
init(location: CLLocation) {
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
}
}
final class LocationManager: NSObject, CLLocationManagerDelegate {
let manager = CLLocationManager()
var onLocationFix: ((Coordinate) -> Void)?
override init() {
super.init()
manager.delegate = self
manager.requestLocation()
}
func getPermission() -> Bool {
switch CLLocationManager.authorizationStatus() {
case .authorizedAlways:
return true
case .authorizedWhenInUse:
return true
case .denied:
return false
case .restricted:
return false
case .notDetermined:
manager.requestWhenInUseAuthorization()
return getPermission()
}
}
//MARK: CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
let coordinate = Coordinate(location: location)
if let onLocationFix = onLocationFix {
onLocationFix(coordinate)
}
}
}
How Can I?
How can I show The AlertController if the privacy is Denied?
With this setup I'm having this error: Warning: Attempt to present <UIAlertController: 0x145ae7ee0> on <xxx.xxController: 0x143e04720> whose view is not in the window hierarchy!.
How can I code the setting button pointing to Settings?
How can I code: "from settings page, I could return to the app"?

viewDidLoad is called after first access self.view property. This mean that for first is called viewDidLoad after this self.view is added on window hierarchy. Move checking code to viewDidAppear(_:) function and be sure that your view controller is presented.
Code that open Settings app seems to be ok, but don't forget to check from which system version it's available.
Your app is not able to interact somehow with other apps while it's in background state.

Related

Using delegate with a static function in Swift

This is a function that I use it in my app, it works perfectly.
class ResenhaEquideosMenuController: UIViewController, CLLocationManagerDelegate {
static let locationManager = CLLocationManager()
func getLocation() {
let status = CLLocationManager.authorizationStatus()
switch status {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
return
case .denied, .restricted:
let alert = UIAlertController(title: "Serviços de localização desativados", message: "Por favor, ative os Serviços de Localização nas Configurações do seu dispositivo", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
return
case .authorizedAlways, .authorizedWhenInUse:
break
}
locationManager.delegate = self
locationManager.startUpdatingLocation() // função didUpdateLocations controla quando GPS capta atualização do Sensor
}
}
But I wanna change this function to a static function just like
static func getLocation() { // some code }
But I got en error on this part of the code
locationManager.delegate = self
Cannot assign value of type 'ResenhaEquideosMenuController.Type' to type 'CLLocationManagerDelegate?'
How can I fix that?
Static functions don't depend on any particular instance of the type that they belong to, so referencing self from inside one as you're doing:
locationManager.delegate = self
doesn't make any sense. self represents a particular object that provides context for the function call, and that's not available to a static function.
How can I fix that?
You're going to have to reconsider your reason for wanting to make getLocation static, and find a different approach.
I didn't find a way to use the delegate with a static function to fix my problem with the CCLocationManager class.
So, based in #DuncanC comment, I create a new class that extends a UIViewController called LocationViewController.
import Foundation
import CoreLocation
class LocationViewController: UIViewController, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
print("___ LocationViewController viewDidLoad")
getLocation()
}
func getLocation() {
let status = CLLocationManager.authorizationStatus()
switch status {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
return
case .denied, .restricted:
let alert = UIAlertController(title: "Serviçoõs de localização desativados", message: "Por favor, ative os Serviços de Localização nas Configurações do seu dispositivo", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
return
case .authorizedAlways, .authorizedWhenInUse:
break
#unknown default:
print("erro desconhecido")
}
locationManager.delegate = self
locationManager.startUpdatingLocation() // função didUpdateLocations controla quando GPS capta atualização do Sensor
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let currentLocation = locations.last {
manager.distanceFilter = 50 // distance changes you want to be informed about (in meters)
manager.desiredAccuracy = 10 // biggest approximation you tolerate (in meters)
//manager.activityType = .automotiveNavigation // .automotiveNavigation will stop the updates when the device is not moving
saveLocationPosition(currentLocation)
}
}
fileprivate func saveLocationPosition(_ currentLocation: CLLocation) {
UserDefaults.standard.set(currentLocation.coordinate.latitude, forKey: RESconstantes.LATITUDE_USER)
UserDefaults.standard.set(currentLocation.coordinate.longitude, forKey: RESconstantes.LONGITUDE_USER)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("locationManager error")
print(error)
}
}
When I want to capture the location of the GPS sensor, I just extend a UIViewController using the LocateViewController.
Just like that example below:
class ResenhaEquideosMenuController: LocationViewController { ...code }
It works for me.

Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee84eaf60) for my CLLocationManager()

I have no idea why this started or how to solve it. The error: Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee84eaf60) occurs on my line:
let locationManager = CLLocationManager()
I declared it in my class above viewDidLoad(). I've run this simulator a ton of times without this problem, and it randomly popped up now. I checked to make sure it wasn't the most recent code I added by running it again without said code, and the error persists. Just in case you think of this, it's not that my default location is set to none, I've had that error before and fixed it. Here are all the parts of my code where my locationManager is used:
let locationManager = CLLocationManager()
then:
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
mapView.delegate = self
mapView.showsUserLocation = true
tapToAddJumpSpotLabel.isHidden = true
}
then:
#IBAction func allowLocationButtonPressed(_ sender: UIBarButtonItem) {
switch CLLocationManager.authorizationStatus() {
// Case when authorization not determined.
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
// Case when authorization granted.
case .authorizedAlways, .authorizedWhenInUse:
let alertController = UIAlertController(title: "Location Access Granted", message: "We already have access to your location. If you want to change this, go to your settings app, and change our location access.", preferredStyle: .alert)
let okayAction = UIAlertAction(title: "Okay", style: .cancel, handler: nil)
alertController.addAction(okayAction)
self.present(alertController, animated: true, completion: nil)
// Case when authorization denied or restricted.
case .restricted, .denied:
let alertController = UIAlertController(title: "Location Access Disabled", message: "We need access to your location in order to provide you with cliff jumping spots.", preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
let openAction = UIAlertAction(title: "Open Settings", style: .default) { (action) in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
alertController.addAction(cancelAction)
alertController.addAction(openAction)
self.present(alertController, animated: true, completion: nil)
// Default (Apple has possible future cases to be added to CLAuthorizationStatus).
#unknown default:
return
}
}
finally:
//MARK: - CLLocationManagerDelegate Methods
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
if let error = error as? CLError, error.code == .denied {
locationManager.stopUpdatingLocation()
print(error)
return
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let lastLocation = locations.last!
mapView.centerCoordinate.latitude = lastLocation.coordinate.latitude
mapView.centerCoordinate.longitude = lastLocation.coordinate.longitude
}
}
I have no idea what is causing this so any help is appreciated :)
Well, after some further digging I feel stupid. Simply an old object initialization of one class in another, and vice versa, causing an infinite loop. Good thing is one of them was old and I just forgot to delete it, so there ya go!

Reevaluate CLLocationManager.authorizationStatus in running app after app location settings change

Thanks to Thomas's advice below I've modified my code from the original request. I'm wondering if I'm trying to do something impossible or, if not, how I could accomplish this.
If the user hasn't authorized location services, I am prompting them via an alert with an "Open Settings" button to change the app's location settings. This works. But upon return from settings to app, I'd like to recognize if the change was made and activate location services. Can this be done? The closure below successfully gets the user to the app's settings, the user can make changes, and the user can return, but the closure fires when the user presses "Open Settings", which is before settings have been changed. BTW: If there's a better way to handle nudging the user to approve an app's currently unauthorized location prefs, I'd appreciate advice. Thanks!
locationManager.requestWhenInUseAuthorization() // request authorization
let authStatus = CLLocationManager.authorizationStatus()
switch authStatus {
case .denied:
let alertController = UIAlertController(title: "Background Location Access Disabled", message: "In order to show the location weather forecast, please open this app's settings and set location access to 'While Using'.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alertController.addAction(UIAlertAction(title: "Open Settings", style: .`default`, handler: { action in
if #available(iOS 10.0, *) {
let settingsURL = URL(string: UIApplicationOpenSettingsURLString)!
UIApplication.shared.open(settingsURL, options: [:], completionHandler: {(success) in
print("*** Success closure fires")
let newAuthStatus = CLLocationManager.authorizationStatus()
switch newAuthStatus {
case .authorizedWhenInUse:
self.locationManager.startUpdatingLocation()
default:
print("Not .authorizedWhenInUse")
}
})
} else {
if let url = NSURL(string:UIApplicationOpenSettingsURLString) {
UIApplication.shared.openURL(url as URL)
}
}
}))
self.present(alertController, animated: true, completion: nil)
case .authorizedWhenInUse, .authorizedAlways:
locationManager.startUpdatingLocation()
case .restricted :
print("App is restricted, likely via parental controls.")
default:
print("UH!! WHAT OTHER CASES ARE THERE? ")
}
I think the extension code here accomplishes what I want - a simple get location, but which handles all authorization status cases:
- .notDetermined: requestWhenInUseAuthorization
- .authorized: startUpdatingLocations
- .denied: Prompt user with an alert that uses UIApplicationOpenSettingsURLString to open the app's Privacy / Location settings so they can make the change. A return to the app with new status is picked up in didUpdateLocations so user location is captured after updating settings
- .restricted - an alert is shown prompting user to check with parent or system administrator to lift app restrictions.
Alert also shows w/error code if didFailWithError.
Just set up instance variables for locationManager & currentLocation
let locationManager = CLLocationManager()
var currentLocation: CLLocation!
and in viewDidLoad set the delegate & call getLocation()
locationManager.delegate = self
getLocation()
Hopefully this is sound, but recs on better ways to do this are most welcome. Hope it helps someone, as I struggled to find something that was in Swift 3 & comprehensive. Thanks again, Thomas!
extension ViewController: CLLocationManagerDelegate {
func getLocation() {
let status = CLLocationManager.authorizationStatus()
handleLocationAuthorizationStatus(status: status)
}
func handleLocationAuthorizationStatus(status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .authorizedWhenInUse, .authorizedAlways:
locationManager.startUpdatingLocation()
case .denied:
print("I'm sorry - I can't show location. User has not authorized it")
statusDeniedAlert()
case .restricted:
showAlert(title: "Access to Location Services is Restricted", message: "Parental Controls or a system administrator may be limiting your access to location services. Ask them to.")
}
}
func showAlert(title: String, message: String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let defaultAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(defaultAction)
present(alertController, animated: true, completion: nil)
}
func statusDeniedAlert() {
let alertController = UIAlertController(title: "Background Location Access Disabled", message: "In order to show the location weather forecast, please open this app's settings and set location access to 'While Using'.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alertController.addAction(UIAlertAction(title: "Open Settings", style: .`default`, handler: { action in
if #available(iOS 10.0, *) {
let settingsURL = URL(string: UIApplicationOpenSettingsURLString)!
UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil)
} else {
if let url = NSURL(string:UIApplicationOpenSettingsURLString) {
UIApplication.shared.openURL(url as URL)
}
}
}))
self.present(alertController, animated: true, completion: nil)
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
handleLocationAuthorizationStatus(status: status)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let currentLocation = locations.last {
print("My coordinates are: \(currentLocation.coordinate.latitude), \(currentLocation.coordinate.longitude)")
locationManager.stopUpdatingLocation()
updateUserInterface()
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
showAlert(title: "Location Access Failure", message: "App could not access locations. Loation services may be unavailable or are turned off. Error code: \(error)")
}
}

Swift - Core location request permission if not granted

Is there any way to ask users to give permission for location detection, if they denied when they were first asked?
For getting user's current location you need to declare:
let locationManager = CLLocationManager()
In viewDidLoad():
// Ask for Authorisation from the User.
self.locationManager.requestAlwaysAuthorization()
// For use in foreground
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
Then in CLLocationManagerDelegate method you can get user's current location coordinates:
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
var locValue:CLLocationCoordinate2D = manager.location.coordinate
print("locations = \(locValue.latitude) \(locValue.longitude)")
}
If user has denied the location then give him option to change location permission from settings:
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print("error::: \(error)")
locationManager.stopUpdatingLocation()
let alert = UIAlertController(title: "Settings", message: "Allow location from settings", preferredStyle: UIAlertControllerStyle.Alert)
self.presentViewController(alert, animated: true, completion: nil)
alert.addAction(UIAlertAction(title: TextMessages().callAlertTitle, style: .Default, handler: { action in
switch action.style{
case .Default: UIApplication.sharedApplication().openURL(NSURL(string: UIApplicationOpenSettingsURLString)!)
case .Cancel: print("cancel")
case .Destructive: print("destructive")
}
}))
}
NOTE:
Make sure to add the Privacy key (NSLocationAlwaysAndWhenInUseUsageDescription) to Info.plist file.
No, you can just remind them inside app within an alert, like "For better usage, we will suggest you to give us the permissions to use your location". And to add button "Go to settings" that will redirect the user to the Location Permissions for your application.

Error: 'Delegate must respond to locationManager:didUpdateLocations:' when collecting user location

So I want to collect user location when a user tap's a button, and use that location later on in my app. With my current implementation, when the button is tapped, the user should be prompted to allow location to be shared and then they should be logged in to the app (anonymously via Firebase in case you are wondering), and the users location information should print to the console. The prompt works, however after the allow location button is hit, my application terminates due to uncaught exception 'NSInternalInconsistencyException'
and the reason is that the 'Delegate must respond to locationManager:didUpdateLocations:'
Which is strange because I do have a didUpdateLocations: function in my code.
I have no idea what is going on, and any help is appreciated. My ViewController code is below, thanks!
/*
* Copyright (c) 2016 Ahad Sheriff
*
*/
import UIKit
import Firebase
import CoreLocation
class LoginViewController: UIViewController {
// MARK: Properties
var ref: FIRDatabaseReference! // 1
var userID: String = ""
var locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().reference() // 2
}
#IBAction func loginDidTouch(sender: AnyObject) {
//ask user to enable location services
locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
//collect user's location
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
locationManager.requestLocation()
locationManager.startUpdatingLocation()
let location = self.locationManager.location
var latitude: Double = location!.coordinate.latitude
var longitude: Double = location!.coordinate.longitude
print("current latitude :: \(latitude)")
print("current longitude :: \(longitude)")
//Log in to application anonymously using Firebase
FIRAuth.auth()?.signInAnonymouslyWithCompletion() { (user, error) in
if let user = user {
print("User is signed in with uid: ", user.uid)
self.userID = user.uid
} else {
print("No user is signed in.")
}
self.performSegueWithIdentifier("LoginToChat", sender: nil)
}
}
else {
print("Unable to determine location")
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
super.prepareForSegue(segue, sender: sender)
let navVc = segue.destinationViewController as! UINavigationController // 1
let chatVc = navVc.viewControllers.first as! ChatViewController // 2
chatVc.senderId = userID // 3
chatVc.senderDisplayName = "" // 4
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!)
{
//--- CLGeocode to get address of current location ---//
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)->Void in
if (error != nil)
{
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
if placemarks!.count > 0
{
let pm = placemarks![0] as CLPlacemark
self.displayLocationInfo(pm)
}
else
{
print("Problem with the data received from geocoder")
}
})
}
func displayLocationInfo(placemark: CLPlacemark?)
{
if let containsPlacemark = placemark
{
//stop updating location
locationManager.stopUpdatingLocation()
let locality = (containsPlacemark.locality != nil) ? containsPlacemark.locality : ""
let postalCode = (containsPlacemark.postalCode != nil) ? containsPlacemark.postalCode : ""
let administrativeArea = (containsPlacemark.administrativeArea != nil) ? containsPlacemark.administrativeArea : ""
let country = (containsPlacemark.country != nil) ? containsPlacemark.country : ""
print(locality)
print(postalCode)
print(administrativeArea)
print(country)
}
}
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
print("Error while updating location " + error.localizedDescription)
}
}
First, add explicitly that you confirm CLLocationManagerDelegate:
class LoginViewController: UIViewController, CLLocationManagerDelegate
Second, set up delegate property for CLLocationManager:
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
ref = FIRDatabase.database().reference()
}
Third, in CLLocationManagerDelegate doc I see different from your declaration of didUpdateLocations and didFailWithError method:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
func locationManager(_ manager: CLLocationManager, didFailWithError error: NSError)
Also, you have few other issues in code. They are all about unsafe unwrapping. You should not use unsafe unwrapping ! everywhere. Doing this you are killing optionals philosophy. Doing right thing, on other hand, can drastically increase stability of your app. In loginDidTouch function make following corrections:
var latitude: Double? = location?.coordinate.latitude
var longitude: Double? = location?.coordinate.longitude
When you call this function first time, your location not determined yet (it will be determined asynchronously, you will get location in delegate method), thats why you have fatal error when used force unwrapp. In didUpdateLocations function:
if let pm = placemarks?.first
{
self.displayLocationInfo(pm)
}
correction of this part of code is shorter then your original code, and prevents you from two possible crashes at one time - when placemarks is nil and when placemarks is not nil, but empty array