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

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 ?

Related

Open SwiftUI view only if location permission granted

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.

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

Getting precise and accurate location updates indoors on the Apple Watch using Core Location?

I'm trying to get location updates for workout tracking (indoors) and so I am in need of very precise and constant location updates, but in testing the delegate callbacks don't seem to be very accurate. For example, moving 20-30 feet doesn't trigger a location update most of the time. Is there anything in my code below that might cause this inaccuracy?
import CoreLocation
protocol UserLocationDelegate: class {
func didUpdateUserLocation(_ manager: WorkoutLocationManager, distance: CLLocationDistance)
}
class WorkoutLocationManager: NSObject, CLLocationManagerDelegate {
deinit {
self.locationManager?.stopUpdatingLocation()
}
private var locationManager: CLLocationManager?
var previousLocation: CLLocation?
weak var userLocationDelgate: UserLocationDelegate?
public func getUserLocation() {
guard CLLocationManager.locationServicesEnabled() else {
print("User does not have location services enabled")
return
}
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.allowsBackgroundLocationUpdates = true
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
locationManager?.activityType = .fitness //test as the docs say this will turn OFF indoor tracking
let locationAuthorizationStatus = CLLocationManager.authorizationStatus()
switch locationAuthorizationStatus {
case .authorizedAlways:
print("location authorized Always")
locationManager?.startUpdatingLocation()
case .authorizedWhenInUse:
print("location authorized When in Use")
locationManager?.startUpdatingLocation()
case .denied:
print("location authorization denied")
locationManager?.requestAlwaysAuthorization()
case .notDetermined:
print("location authorization not determined")
locationManager?.requestAlwaysAuthorization()
case .restricted:
print("location authorization restricted")
locationManager?.requestAlwaysAuthorization()
}
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("did update locations called")
if previousLocation == nil {
previousLocation = locations.first
} else {
guard let latest = locations.first else { return }
let distanceInMeters = previousLocation?.distance(from: latest) ?? 0
if distanceInMeters > 0 {
let distanceInFeet = distanceInMeters * 3.28
print("distance in feet: \(distanceInFeet)")
userLocationDelgate?.didUpdateUserLocation(self, distance: distanceInFeet
)
}
previousLocation = latest
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("location manager error = \(error)")
}
}
import WatchKit
import Foundation
import CoreLocation
class InterfaceController: WKInterfaceController, UserLocationDelegate {
func didUpdateUserLocation(_ manager: WorkoutLocationManager, distance: CLLocationDistance) {
locationLabel.setText("\(distance.rounded().description) feet")
}
let workoutLocationManager = WorkoutLocationManager()
#IBOutlet weak var locationLabel: WKInterfaceLabel!
override func awake(withContext context: Any?) {
super.awake(withContext: context)
workoutLocationManager.getUserLocation()
workoutLocationManager.userLocationDelgate = self
}
You set kCLLocationAccuracyBest as desiredAccuracy. There is variable for CLLocationAccuracy which should be more accurate
kCLLocationAccuracyBestForNavigation

Issue with CLLocationManager and CLLocationManagerDelegate

I'm running Xcode 10 and iOS 12
I'm getting this warning on each of the delegate methods coded in the class extension of my CLLocationManager singleton:
Instance method 'locationManager(:didChangeAuthorization:)' nearly matches optional requirement 'locationManager(:didChangeAuthorization:)' of protocol 'CLLocationManagerDelegate'
Here is the code:
import Foundation
import CoreLocation
public class PhysicalLocationManager: NSObject {
/*--------------------------------------------------------------------------------*/
//MARK: - Create Singleton Shared Instance
/*--------------------------------------------------------------------------------*/
static let sharedInstance: PhysicalLocationManager = {
let instance = PhysicalLocationManager()
return instance
}()
let locationMgr: CLLocationManager
/*--------------------------------------------------------------------------------*/
//MARK: - Initialization
/*--------------------------------------------------------------------------------*/
override init() {
locationMgr = CLLocationManager()
locationMgr.distanceFilter = kCLDistanceFilterNone
locationMgr.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
super.init()
locationMgr.delegate = self
}
func enableBasicLocationServices() {
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
// Request when-in-use authorization initially
locationMgr.requestWhenInUseAuthorization()
break
case .restricted, .denied:
// Disable location features
// TODO: disableMyLocationBasedFeatures()
break
case .authorizedWhenInUse, .authorizedAlways:
// Enable location features
enableWhenInUseFeatures()
break
}
}
func enableWhenInUseFeatures() {
locationMgr.startUpdatingLocation()
if CLLocationManager.locationServicesEnabled() {
locationMgr.requestLocation()
}
}
}
extension PhysicalLocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
print("\(manager)\tCLLocationManager, didChangeAuthorization\n\(status)")
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("\(manager)\tCLLocationManager, didUpdateLocations\n\(locations)")
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
// locationManager.stopUpdatingLocation()
}
}
Can anyone see what I am doing wrong here?
Thanks,
Because your PhysicalLocationManager class is public, the delegate methods need to be public as well. Simply add public in front of the three delegate methods and the warnings go away.

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.

Resources