CoreLocation Delegate functions do not run until view controller viewDidLoad function is finished - ios

This is more than likely something very easy to solve but I've been at it for some time and can't seem to get at an answer.
I would like to know why a Delegate method for CLLocationManager does not trigger until after the ViewDidLoad function when the CLLocationManager is loaded within the ViewDidLoad function.
I have set my default region to Sydney Australia within my App Scheme and I have encapsulated my locationManager within its own class as follows:
import UIKit
import CoreLocation
/* Class location is a class to track user location and return a location object. */
class usrLocation: NSObject, CLLocationManagerDelegate
{
//MARK: Properties
var locationMgr: CLLocationManager!
var location: CLLocation!
var seenError: Bool = false
//MARK: Public Methods
func startTracking() {
locationMgr = CLLocationManager()
locationMgr.delegate = self
locationMgr.desiredAccuracy = kCLLocationAccuracyBest
locationMgr.requestWhenInUseAuthorization()
locationMgr.startUpdatingLocation()
}
//Return a location object
func getLocation() -> CLLocation {
locationMgr.startUpdatingLocation()
return location!
}
//MARK: CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
locationMgr.stopUpdatingLocation()
if (seenError == false) {
seenError = true
print(error)
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
location = (locations ).last
locationMgr.stopUpdatingLocation()
}
}
I have initialised the class in my ViewController and try to begin tracking my current location in viewDidLoad.
The code looks like this.
override func viewDidLoad() {
var location = usrLocation()
override func viewDidLoad() {
super.viewDidLoad()
// Track location
location.startTracking()
location.getLocation()
//update Label text
sLongitude.text = "\(location.getLocation().coordinate.longitude)"
sLatitude.text = "\(location.getLocation().coordinate.latitude)"
}
getLocation() never returns location as it is always nil as the delegate didUpdateLocations function does not run. Why is this?

I tried to get this working using a completion handler but couldn't so my solution has been to ignore setting the label text via viewDidLoad and to instead update the labels once the location variable has been set. That way viewDidLoad completes, the delegate gets called and the labels get updated.
var location: CLLocation! {
didSet {
//update Label text
sLongitude.text = "\(location.getLocation().coordinate.longitude)"
sLatitude.text = "\(location.getLocation().coordinate.latitude)"
}
}

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.

Swift 3 - store the User Location and call it from different View Controllers

I'm pretty new in programming and this is my first app, so sorry if the approach is very shabby.
I created a helper method to get the user location, because I need to call it from different view controllers so I thought this was a cleaner way to do it. But I don't know why is not working now (no errors, it just show the general view of Europe). But when it was inside the view controller it worked perfectly fine.
I got this new approach from the course I'm doing and I've been researching in many sources. I've also checked this question but I didn't find any solution yet.
Here is the method I created in the GMSClient file. It will get the user location, but if the user disables this option, it will show the default position (centred in Berlin):
extension GMSClient: CLLocationManagerDelegate {
//MARK: Initial Location: Berlin
func setDefaultInitialLocation(_ map: GMSMapView) {
let camera = GMSCameraPosition.camera(withLatitude: 52.520736, longitude: 13.409423, zoom: 8)
map.camera = camera
let initialLocation = CLLocationCoordinate2DMake(52.520736, 13.409423)
let marker = GMSMarker(position: initialLocation)
marker.title = "Berlin"
marker.map = map
}
//MARK: Get user location
func getUserLocation(_ map: GMSMapView,_ locationManager: CLLocationManager) {
var userLocation: String?
locationManager.requestWhenInUseAuthorization()
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
map.isMyLocationEnabled = true
map.settings.myLocationButton = true
} else {
setDefaultInitialLocation(map)
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
map.camera = GMSCameraPosition(target: location.coordinate, zoom: 15, bearing: 0, viewingAngle: 0)
locationManager.stopUpdatingLocation()
//Store User Location
userLocation = "\(location.coordinate.latitude), \(location.coordinate.longitude)"
print("userLocation is: \((userLocation) ?? "No user Location")")
}
}
}
}
This file has also this singelton:
// MARK: Shared Instance
class func sharedInstance() -> GMSClient {
struct Singleton {
static var sharedInstance = GMSClient()
}
return Singleton.sharedInstance
}
And then I call it in my view controller like this:
class MapViewController: UIViewController, CLLocationManagerDelegate {
// MARK: Outlets
#IBOutlet weak var mapView: GMSMapView!
// MARK: Properties
let locationManager = CLLocationManager()
var userLocation: String?
let locationManagerDelegate = GMSClient()
// MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
self.locationManager.delegate = locationManagerDelegate
GMSClient.sharedInstance().getUserLocation(mapView, locationManager)
}
Anyone has an idea of what can be wrong?
Thanks!
Following what Paulw11 said, I found the faster solution using Notifications.
Send notification from the LocationManager delegate method inside the first view Controller:
class MapViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
mapView.isMyLocationEnabled = true
mapView.settings.myLocationButton = true
} else {
initialLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
mapView.camera = GMSCameraPosition(target: location.coordinate, zoom: 15, bearing: 0, viewingAngle: 0)
locationManager.stopUpdatingLocation()
let userInfo : NSDictionary = ["location" : location]
NotificationCenter.default.post(name: NSNotification.Name("UserLocationNotification"), object: self, userInfo: userInfo as [NSObject : AnyObject])
}
}
}
Set the second view controller as observer. This way I can store the userLocation and use it later for the search request:
class NeighbourhoodPickerViewController: UIViewController, UITextFieldDelegate {
var userLocation: String?
var currentLocation: CLLocation!
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(locationUpdateNotification), name: Notification.Name("UserLocationNotification"), object: nil)
}
func locationUpdateNotification(notification: NSNotification) {
if let userInfo = notification.userInfo?["location"] as? CLLocation {
self.currentLocation = userInfo
self.userLocation = "\(userInfo.coordinate.latitude), \(userInfo.coordinate.longitude)"
}
}
I guess the problem is here,
self.locationManager.delegate = locationManagerDelegate
You have created a new instance of GMSClient, and saved it in the stored property and that instance is set as the delegate property of CLLocationManager.
You need to do this instead,
self.locationManager.delegate = GMSClient.sharedInstance()
You need to do this because you would want singleton instance of GMSClient to be the delegate for CLLocationManager and not a new instance. That way your singleton class would recieve the callbacks from
CLLocationManager class.
To understand more about why your code was not working, I would suggest you read more about Objects, Instances, Instance variables, Singletons, Delegate design pattern.

Error creating a global CLLocationManager

Apple Docs suggest not to store your CLLocationManager in a local variable. So I created a global constant outside the scope of my ViewController class just after the import statements. Trying to access the constant inside the class, however, throws compiler errors:
A similarly declared globalDictionaryconstant of type NSMutableDictionary seems to be accessible inside the class.
What is it that I am doing wrong here? Why does the above not work?
Code:
//
// ViewController.swift
// CoreLocationExample
//
//
import UIKit
import CoreLocation
import Foundation
let locationManager = CLLocationManager()
let globalDictionary = NSMutableDictionary()
class ViewController: UIViewController, CLLocationManagerDelegate {
// let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
let dict = [
"name":"John"
]
globalDictionary.addEntries(from: dict)
print(globalDictionary)
}
}
Using Swift 3 on Xcode Version 8.3.1
Apple Docs suggest not to store your CLLocationManager in a local variable.
It means do not create local instance inside a method / function.
please declare like this.
class AppDelegate: UIResponder, UIApplicationDelegate {
//CLLocation Manager
let locationManager = CLLocationManager()
var locValue = CLLocationCoordinate2D()
}
CLLocation Manager or globalDictionary Inside the class.
use singleton class for this
import UIKit
import CoreLocation
public class LocationManager: NSObject,CLLocationManagerDelegate {
public static let sharedInstance = LocationManager()
public lazy var locationManager: CLLocationManager = CLLocationManager()
var globalDictionary = NSMutableDictionary()
public override init() {
super.init()
self.locationManager.requestAlwaysAuthorization()
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.distanceFilter = kCLDistanceFilterNone;
self.locationManager.startUpdatingLocation()
self.locationManager.delegate = self
let dict = [
"name":"John"
]
self.globalDictionary.addEntries(from: dict)
print(self.globalDictionary)
}
// MARK: - Location Manager Delegate
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]){
let newLocation: CLLocation? = locations.last
let locationAge: TimeInterval? = -(newLocation?.timestamp.timeIntervalSinceNow)!
if Double(locationAge!) > 5.0 {
return
}
if Double((newLocation?.horizontalAccuracy)!) < 0 {
return
}
}
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
}
}
You can do it in the following way -
class LocationTracker {
static var locationManager: CLLocationManager? = LocationTracker.sharedLocationManager()
class func sharedLocationManager() -> CLLocationManager {
let lockQueue = DispatchQueue(label: "self")
lockQueue.sync {
if _locationManager == nil {
_locationManager = CLLocationManager()
_locationManager?.desiredAccuracy = kCLLocationAccuracyBestForNavigation
_locationManager?.allowsBackgroundLocationUpdates = true
_locationManager?.pausesLocationUpdatesAutomatically = false
}
}
return _locationManager!
}
}
And when you need to call it in your code, you can do the below -
var locationManager: CLLocationManager? = LocationTracker.sharedLocationManager()
you can then add other Location related methods in this class as well like for startt tracking, update location, stoptracking etc.

My simple map project doesn't get & show my location in simulator

I am using XCode v7.2.1, Simulator v9.2 .
I have a UIViewController which shows a map & is supposed to get my location & show it on map:
import UIKit
import MapKit
class LocationVC: UIViewController, MKMapViewDelegate {
#IBOutlet weak var map: MKMapView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
map.delegate = self
}
override func viewDidAppear(animated: Bool) {
if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {
map.showsUserLocation = true
} else {
locationManager.requestWhenInUseAuthorization()
}
}
}
I have added the NSLocationWhenInUseUsageDescription in info.plist as shown below:
I have also selected the Debug -> Location -> Custom Location ... and set the longitude & latitude of Helsinki, Finland as shown below:
When I run my app, the map is shown, however it doesn't get my location. Why? (I mean I don't see the blue point in anywhere of the map).
===== UPDATE ====
I also tried this when my app is running, however it doesn't help either.
you are requesting the user's location, but not actually doing anything with the response. become the delegate of the location manager and respond to the authorization change.
this code works for me on 7.2.1 (after selecting "Apple" in Debug -> Location):
import UIKit
import MapKit
class ViewController: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var map: MKMapView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {
map.showsUserLocation = true
} else {
locationManager.requestWhenInUseAuthorization()
}
}
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
guard status == .AuthorizedWhenInUse else { print("not enabled"); return }
map.showsUserLocation = true
}
}
I agree with #Casey 's answer,but sometimes you need to do a little more with CLLocationManagerDelegate method.
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
//reset mapView's center in case your custom location was wrong.
map.centerCoordinate = location.coordinate
//mannual call show annotations to avoid some bugs
map.showAnnotations(map.annotations, animated: true)
}
}
you just have to add
locationManager.delegate = self
mapView.showsUserLocation = true

Swift - User Location Issues

I've just started with swift and I'm having an issue. I've read the various threads about user location and map kits and can't solve my issue. I had the code running and could create regions as I wanted and I could zoom into the user location.
I've paired the code back to try and locate the issue and the code left is below. The issue is that the userlocation is coming back as a nil value when you try and run the simulator which crashes the app. What am I doing wrong as I've completed authorising user location so surely it shouldn't be coming back nil. At one point I had code to zoom on the user location AFTER initially setting a region elsewhere and calling a function to do the zoom, but if you initially try and call the user location its always nil so you can't initialise the map zooming into where the user is which is what I want.
import UIKit
import MapKit
class MapController: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
// MARK: - location manager to authorize user location for Maps app
var locationManager = CLLocationManager()
func checkLocationAuthorizationStatus() {
if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {
mapView.showsUserLocation = true
} else {
locationManager.requestWhenInUseAuthorization()
}
}
override func viewDidLoad() {
super.viewDidLoad()
checkLocationAuthorizationStatus()
var userLocation = locationManager.location
println("\(userLocation.coordinate.latitude)")
println("\(userLocation.coordinate.longitude)")
// Do any additional setup after loading the view.
}
}
Firstly, CLLocationManager updates user location asynchronously. That means that even after you call startUpdatingLocation() your location will be nil until location manager returns with the new location.
Secondly, in your code you are not actually calling this method. If you DO need to be able to store the user location then you should change your code to:
import UIKit
import MapKit
class MapController: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
// MARK: - location manager to authorize user location for Maps app
lazy var locationManager: CLLocationManager = {
var manager = CLLocationManager()
manager.delegate = self
return manager
}()
func checkLocationAuthorizationStatus() {
if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {
mapView.showsUserLocation = true
locationManager.startUpdatingLocation()
} else {
locationManager.requestWhenInUseAuthorization()
}
}
override func viewDidLoad() {
super.viewDidLoad()
checkLocationAuthorizationStatus()
//location is nil at this point because location update is
//an asynchronous operation!
//var userLocation = locationManager.location
//println("\(userLocation.coordinate.latitude)")
//println("\(userLocation.coordinate.longitude)")
// Do any additional setup after loading the view.
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
//this is the place where you get the new location
println("\(location.coordinate.latitude)")
println("\(location.coordinate.longitude)")
}
}
There is only one minor thing to note. In the last function I am using an argument locations: [CLLocation]. This is definitely correct in Swift 2.0, but in Swift 1.2 it might be locations: [AnyObject] in which case you have to do a conditional downcast yourself.
Let me know if this works for you

Resources