Open SwiftUI view only if location permission granted - ios

I have a view (say V) in which a user answers a few questions and their location is recorded. However, the answers only make sense with the user's location.
So what I want is that when the user clicks on a button on the parent view, it takes them to V and immediately asks them for the location permission. If they accept, they can continue on to answer the questions, but if they deny, they navigate back to the parent screen.
I know I can navigate back to the parent screen with self.presentation.wrappedValue.dismiss().
But how do I know when the user has accepted or denied the permission since requestWhenInUseAuthorization() is an asynchronous function?
I'm following this tutorial on getting a user's location on iOS with Swift.
Code for my LocationService:
import CoreLocation
protocol LocationServiceDelegate {
func didFetchCurrentLocation(_ location: GeoLocation)
func fetchCurrentLocationFailed(error: Error)
}
class LocationService: NSObject, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
var delegate: LocationServiceDelegate
init(delegate: LocationServiceDelegate) {
self.delegate = delegate
super.init()
self.setupLocationManager()
}
private func setupLocationManager() {
if canUseLocationManager() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
}
func requestLocation() {
if canUseLocationManager() {
print(CLAuthorizationStatus.self)
locationManager.requestWhenInUseAuthorization()
locationManager.requestLocation()
}
}
func requestPermission() {
locationManager.requestWhenInUseAuthorization()
}
private func canUseLocationManager() -> Bool {
return CLLocationManager.locationServicesEnabled()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print(locations)
if let location = locations.last {
let geoLocation = GeoLocation(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
delegate.didFetchCurrentLocation(geoLocation)
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
delegate.fetchCurrentLocationFailed(error: error)
}
deinit {
locationManager.stopUpdatingLocation()
}
}
struct GeoLocation {
var latitude: Double
var longitude: Double
}

CLLocationManagerDelegate has also the following method:
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
}
This method is called every time the authorization status changed. I would also like to recommend you implementing your LocationService as an ObservableObject instead of using delegate approach.

Related

Getting location on real device not working

I'm trying to get the user location, running on the simulator, I get the default address, but atleast I know it is working.
I tried to run it on my device but it didn't work.
I try to look for a solution before writing this question but couldn't find something that work for me.
This is my code:
LocationManager:
class LocationManager: NSObject, CLLocationManagerDelegate {
static let shared = LocationManager()
var locationManager: CLLocationManager!
var geoCoder = CLGeocoder()
var callBack:((String)->())?
override init() {
super.init()
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.activityType = .other
locationManager.requestWhenInUseAuthorization()
}
func checkIfLocationIsEnabled() -> Bool{
return CLLocationManager.locationServicesEnabled()
}
func getUserLocation(){
locationManager.requestLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]){
let userLocation: CLLocation = locations[0] as CLLocation
geoCoder.reverseGeocodeLocation(userLocation) { (placemarks, err) in
if let place = placemarks?.last{
self.callBack?(place.name!)
}
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
}
}
This is my getLocation (just calling the getUserLocation and setting the address I get from the callback):
func getLocation(_ label: UILabel) -> String{
guard let comment = self.mView.addCommentTextField.text else { return ""}
LocationManager.shared.getUserLocation()
var addressString = ""
LocationManager.shared.callBack = { address in
DispatchQueue.main.async {
label.text = "\(address), \(comment)"
addressString = address
}
}
return addressString
}
This is how I call getLocation:
self.mView.inLabel.isHidden = false
self.getLocation(self.mView.inLabel)
Actually looking closer at your code, I see that you are asking permissions like this:
locationManager.requestWhenInUseAuthorization()
But requestWhenInUseAuthorization() is asynchronous call, you need to wait for user response before you can use any location services:
When the current authorization status is CLAuthorizationStatus.notDetermined, this method runs asynchronously and prompts the user to grant permission to the app to use location services.
(source)
Also notice that it will only work if status is notDetermined. Any other status would not trigger it. So firstly:
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
// already authorized, can use location services right away
}
if CLLocationManager.authorizationStatus() == .notDetermined {
locationManager.requestWhenInUseAuthorization()
// wait, don't call any location-related functions until you get a response
}
If location permissions are set to anything else, no point to ask for them.
And then your class is already CLLocationManagerDelegate, so:
func locationManager(_ manager: CLLocationManager,
didChangeAuthorization status: CLAuthorizationStatus) {
// do something with new status, e.g.
if status == .authorizedWhenInUse {
// good, now you can start accessing location data
}
// otherwise, you can't

Location Service as a Singleton in Swift, get stuck on "When In Use"

I'm programming an app that needs "Always location" and I decided to use a Singleton to keep tracking active since I need most of the time the location services even in the background.
When I run the application on my iPhone, the console says that the location service is in "When In Use" mode and my protocol don't get the location updates from the LocationManager.
What's wrong with my Singleton (I'm a Swift newbie please be clear in your answers.
Is it a good idea to use a Singleton for Location Services ?
LocationService.swift (UPDATED)
import Foundation
import CoreLocation
protocol LocationServiceDelegate {
func onLocationUpdate(location: CLLocation)
func onLocationDidFailWithError(error: Error)
}
class LocationService: NSObject, CLLocationManagerDelegate {
public static let shared = LocationService()
var delegate: LocationServiceDelegate?
var locationManager: CLLocationManager!
var currentLocation: CLLocation!
private override init() {
super.init()
self.initializeLocationServices()
}
func initializeLocationServices() {
self.locationManager = CLLocationManager()
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestAlwaysAuthorization()
self.locationManager.pausesLocationUpdatesAutomatically = false
self.locationManager.delegate = self
self.locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .restricted:
print("Location access was restricted.")
case .denied:
print("User denied access to location.")
case .notDetermined:
self.locationManager.requestAlwaysAuthorization()
case .authorizedAlways: fallthrough
case .authorizedWhenInUse:
print("User choosed locatiom when app is in use.")
default:
print("Unhandled error occured.")
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.currentLocation = locations.last!
locationChanged(location: currentLocation)
}
private func locationChanged(location: CLLocation) {
guard let delegate = self.delegate else {
return
}
delegate.onLocationUpdate(location: location)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
self.locationManager.stopUpdatingLocation()
locationFailed(error: error)
}
private func locationFailed(error: Error) {
guard let delegate = self.delegate else {
return
}
delegate.onLocationDidFailWithError(error: error)
}
}
Then I initialize the singleton :
AppDelegate.swift
let locationService = LocationService.shared
Then my View conforms to my protocol :
ViewController.swift
extension ViewController: LocationServiceDelegate {
func onLocationUpdate(location: CLLocation) {
print("Current Location : \(location)")
}
func onLocationDidFailWithError(error: Error) {
print("Error while trying to update device location : \(error)")
}
}
Yes, You can use singleton for your purpose. Few things you can check with your implementation:
locationManager.pausesLocationUpdatesAutomatically = false.
enable background modes for location updates.
Switch to significant location updates when the app moves to background.
Is it a better way to send notifications to all viewControllers to pass the CLLocation object or its better to conform to my protocol in every controllers ?

Google maps - Update User location

I am using Google maps SDK for iOS.
The isMyLocationEnabled is set to true.
Sometimes the use location updated to the right location. But sometimes the location is not updated.
I wanted to know if this option is using the original coordinates of the user? also, there is an option to update the user location? ) I am talking about the blue point.
May be this can help:
import Foundation
import CoreLocation
class Location: NSObject {
static let shared = Location()
var locationManager = CLLocationManager()
func locationManagerSetup() {
// Ask for Authorisation from the User.
locationManager.requestAlwaysAuthorization()
// If location services is enabled get the users location
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestLocation()
locationManager.startUpdatigLocation()
}
}
}
extension Location: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
// do something
manager.stopUpdatingLocation()
}
else {
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
manager.requestLocation()
}
// If we have been deined access give the user the option to change it
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if(status == CLAuthorizationStatus.denied) {
// do something
}
}
}

how to execute an action after allow button is pressed in the location access permission?

I am making an app that uses coordinate from GPS, before implementing it, we have to ask permission to the user like the picture above.
I want to make if the user tap "allow" at that alert, then activateGPSToSearchCoordinate() is trigerred, but if 'don't allow' is tapped then I don't want to do anything.
this is my code at the moment, and it doesn't work properly
class LocationManager: NSObject {
let manager = CLLocationManager()
var didGetLocation: ((Coordinate?) -> Void)?
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestLocation()
}
func getPermission() -> CLAuthorizationStatus {
// to ask permission to the user by showing an alert (the alert message is available on info.plist)
if CLLocationManager.authorizationStatus() == .notDetermined {
manager.requestWhenInUseAuthorization()
return .notDetermined
} else if CLLocationManager.authorizationStatus() == .denied {
return .denied
} else if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
return .authorizedWhenInUse
} else {
return .notDetermined
}
}
}
I will use that class in the view controller method like below, especially that getPermission()
func getCoordinate() {
let coordinateAuthorizationStatus = locationManager.getPermission()
if coordinateAuthorizationStatus == .authorizedWhenInUse {
activateGPSToSearchCoordinate()
} else if coordinateAuthorizationStatus == .denied {
showAlertSetting()
}
}
at the moment, if that permission is triggered for the very first time...
either the user tap 'Allow' or 'don't Allow' the CLAuthorizationStatus will always be .notDetermined
so the activateGPSToSearchCoordinate() , will never be triggered.
so I need to to activate activateGPSToSearchCoordinate() only after the 'Allow' at that alert is pressed
how to solve this problem?
Read more about CLLocationManagerDelegate, there is delegate methods for Success and failure.
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
GlobalObjects.shared.latitude = locations[0].coordinate.latitude
GlobalObjects.shared.longtitude = locations[0].coordinate.longitude
GlobalObjects.shared.locationOBJ = locations[0]
print(GlobalObjects.shared.latitude, GlobalObjects.shared.longtitude)
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
}
What about CLLocationManagerDelegate? Did you try this?
func locationManager(CLLocationManager, didUpdateLocations: [CLLocation])
//Tells the delegate that new location data is available.
func locationManager(CLLocationManager, didFailWithError: Error)
//Tells the delegate that the location manager was unable to retrieve a location value.
func locationManager(CLLocationManager, didFinishDeferredUpdatesWithError: Error?)
//Tells the delegate that updates will no longer be deferred.
func locationManager(CLLocationManager, didUpdateTo: CLLocation, from: CLLocation)
//Tells the delegate that a new location value is available.
Implement CLLocationManagerDelegate, on a class/view controller from where you are calling func getCoordinate().
class TestViewController: CLLocationManagerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
}
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// get location coordinate
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// handle error, if any kind of error occurs
}
Here are nice tutorial with example, may help you, in implementing CLLocationManagerDelegate :
https://developer.apple.com/documentation/corelocation/cllocationmanager
https://www.raywenderlich.com/160517/mapkit-tutorial-getting-started
https://www.appcoda.com/tag/mapkit/
http://www.techotopia.com/index.php/A_Swift_Example_iOS_8_Location_Application

Permission popup appear multiple time

I have a project in swift 2.When I launch the app first time there are three different type of permissions popup (Push notification, Location, Photo) that appear on the splash screen.I have add the Permission for location and photos in info.plist
The problem is when the app lunched the one(location) popup appear and disappear without any click then other(photos) popup appear and disappear after few seconds without any click.After few seconds the popup appear one by one and now the popup are display on the screen until I click any one option.
I want to display the permission popup only once when user tap on the button.I have searched about it but all the solutions that I found are in latest version of swift. Any suggestion regarding this is appreciated.
import CoreLocation
private var locationManager: CLLocationManager!
private var locationHandler: ((location: CLLocation?,error: NSError?) -> Void)?
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
func requestCurrentLocation(completionHandler:((location: CLLocation?,error: NSError?)->Void)!) {
locationHandler = completionHandler
if #available(iOS 9.0, *) {
locationManager.requestLocation()
} else {
locationManager.startUpdatingLocation()
}
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let locationHandler = locationHandler, let location = locations.first {
locationHandler(location: location, error: nil)
self.locationHandler = nil
locationManager.stopUpdatingLocation()
}
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
if let locationHandler = locationHandler {
locationHandler(location: nil, error: error)
self.locationHandler = nil
}
}
The key is CLLocationManager always alive
import CoreLocation
class LocationManager: NSObject {
static let shared = LocationManager()
private lazy var locationManager: CLLocationManager = {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
return manager
}()
private override init() {
super.init()
}
func askForPermission() {
locationManager.requestWhenInUseAuthorization()
}
func requestLocation() {
locationManager.requestLocation()
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
// do something
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// do something
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// do something
}
}

Resources