I am using Firebase and Google Maps within my app for user accounts and for the main map view. In my app, I want it to start at the user's location and then have them be able to move the map around, but there are a few issues.
Once the user is authenticated via. Firebase, I have it try to get the users location, but it usually returns nil even when I have location services enabled for the app. Here's my code for that:
if let mylocation = mapView.myLocation {
print("User's location: \(mylocation)")
} else {
print("User's location is unknown")
}
This is in my viewDidLoad function also. I do have the code for asking permission to use location as well as the Privacy statement in my Info.plist file.
If my map happens to load and the user location is found, I try to have it center the view on the user's location. However, it can't get the user's location in time, or so it seems, and the map loads at some other location and it won't update to the user's location.
So here's my question
Is there any way to load the map and have it update to the user's location without it being stuck there for the entirety of the apps use? An example of what I want is basically a Google Maps clone where it starts at the user's location and they are able to interact with the map without it trying to consistently update the map to the user's location. If not, is there a way to wait for the location to be found before loading the map? I tried using GCD (Grand Central Dispatch), but that didn't work. I feel like that isn't my best option, waiting for the location to load before displaying the map that is, because there are many issues that could happen with it. If neither of these are options, what would you do? Thanks!
If My Location is enabled, reveals where the user location dot is being
drawn. If it is disabled, or it is enabled but no location data is available,
this will be nil. This property is observable using KVO.
Since the myLocation on the GMSMapView is observable with KVO to observe this value should solve your problem.
Here some example code:
Add self as an observer for the myLocation property of your GMSMapView instance.
self.mapview.addObserver(self, forKeyPath: "myLocation", options: NSKeyValueObservingOptions.new, context: nil)
Here you handle the changes:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let update = change, let myLocation = update[NSKeyValueChangeKey.newKey] as? CLLocation else {
return
}
self.mapView.camera = GMSCameraPosition.camera(withTarget: myLocation.coordinate, zoom: self.defaultTrackingZoom)
}
DonĀ“t forget to remove your observer somewhere e.g. deinit or viewWillDisappear
deinit
{
self.mapView.removeObserver(self, forKeyPath: "myLocation")
}
In this example the map position is getting updated every time the user position has changed. If you need that position only once at start you could add a flag like myLocationFound and check that in the observeValue:forKeyPath
Hope that helps.
Related
I have a tracking function but it doesn't update location while in background.
1st case: Tracking while app is in the foreground -> the tracking is actually happening but doesn't get precise coordinates. I will change to locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation to see if improves accuracy of the tracking.
2nd case: Tracking while screen is off -> the tracking is a straight line from a to b, tracking doesn't update coordinates.
3rd case: Tracking while app is in back ground(pressed home button) -> tracking is happening as case 1.
I found a post that explains that if authorisation is set to always you have to specify you want to keep updating location while in background, but nothing has changed. This is the code and info.plist :
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
locationManager.delegate = self
// locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
// locationManager.allowsBackgroundLocationUpdates = true //for getting user location in background mode as well
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow //map following user
configureLocationServices()
addDoubleTap() // enabling duble tap gesture recognizer
// mapView.isUserInteractionEnabled = true
let location = locationManager.location?.coordinate
let region = MKCoordinateRegionMakeWithDistance(location!, 1000, 1000) // set mapView based on user location coordinates
mapView.setRegion(region, animated: true)
centerMapOnLocation()
// alerts coordinates to post to Firebase
let alertDrawLatitude = alertDrawCoordinates?.latitude // not used ?
let alertDrawLomgitude = alertDrawCoordinates?.longitude
let title: String? = alertNotificationType
var subtitle: String? = alertNotificationType
// user alert notification. takes coordinates from alertNotificationArray( populated with firebase returning coordinate for all alerts
displayAlerts()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let mostRecentLocation = locations.last else { return }
self.actualRouteInUseCoordinatesArray.append(mostRecentLocation.coordinate)
}
func configureLocationServices() {
if authorizationStatus == .notDetermined{
locationManager.requestAlwaysAuthorization()
} else if authorizationStatus == .authorizedAlways {
locationManager.showsBackgroundLocationIndicator = true //set update location even if in background. very imposrtant!!
}
}
UPDATE:
changing the accuracy only made things worse.
with AccuracyBest:
and with AccuracyBestForNAvigation
second tracking is actually worse.. how can navigation apps rely on this kind of tracking? is there anything wrong with my code for LocationManager?
SECOND UPDATE:
it now get updated location when in background, but is way off..I never passed the yellow street and it shows like I waked for 10 minutes after it..
THIRD EDIT:
I found out that I should filter out GPS raw data, so I'm using a Kalman filter, and it really smooths out the resulting tracking.
So I'm fine tuning two parameters, and in order to be able to change those parameters I added two textfields #IBOutlet weak var filterValueTextField: UITextField! and #IBOutlet weak var horizontalAccuracyTextField: UITextField!and connected those to the parameters
hcKalmanFilter?.rValue = Double(String( describing:filterValueTextField?.text!))! and guard mostRecentLocation.horizontalAccuracy < Double(String( describing: horizontalAccuracyTextField?.text!))! else { return }.
My problem is now that it finds nil while unwrapping value in the horizontalAccuracy parameter.
If in horizontalAccuracy I just put a value it accepts an integer, but when I take it from the texField converting the textfield.text to Int, compiler throws an error Binary operator '<' cannot be applied to operands of type 'CLLocationAccuracy' (aka 'Double') and 'Int', while if I convert it to Double doesn't, but it finds nil.
Why the filterValue finds a value from it's textField, and the horizontal Accuracy doesn't? they're declared, and use the same way.
Any idea?
First of all you have limited time while your app goes to background, and that time is depend upon load on your device's OS, but most probably it is approx. 30 seconds. So this is the reason your are not getting location updates while your screen is off or while your app goes to background.
But Apple allows app to run in background for some tasks and location update is one of them, so you can fetch location updates even if your app goes to background by enabling Background Fetch capability for your app.
For more details please follow below official doc. of Apple:
https://developer.apple.com/documentation/corelocation/getting_the_user_s_location/handling_location_events_in_the_background
And secondly try to maintain your locationmanager object in global scope of your app like you can place it in AppDelegate or in Singleton class if you are maintaining any for your app, so it will always be available.
Sometimes location that you receive does not have desired accuracy, especially when you've just started tracking, first couple of locations are going to be well off. You can use location's horizontal accuracy property to filter location witch have, for example, less then 50m accuracy
My app starts up at the user's location, however, every time the user moves the map's camera updates to their location. I want it to load to their location, but then allow them to freely browse the map even if their location is moving. Similar to the behavior exhibited in the Google Maps app. I'm using a KVO to gather the location within the viewDidLoad() function. That line looks like this:
mapView.isMyLocationEnabled = true
self.mapView.addObserver(self, forKeyPath: "myLocation", options: NSKeyValueObservingOptions.new, context: nil)
Here's my code of the observe function:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let update = change, let myLocation = update[NSKeyValueChangeKey.newKey] as? CLLocation else {
print("Location not found")
return
}
self.mapView.camera = GMSCameraPosition.camera(withTarget: myLocation.coordinate, zoom: 14)
}
What needs to change to make it meet the criteria mentioned above.
Using this helper code, you can get user's location and set the target like:
CLLocationManager *manager = [CLLocationManager updateManagerWithAccuracy:50.0 locationAge:15.0 authorizationDesciption:CLLocationUpdateAuthorizationDescriptionAlways];
[self.manager startUpdatingLocationWithUpdateBlock:^(CLLocationManager *manager, CLLocation *location, NSError *error, BOOL *stopUpdating) {
self.mapView.camera = GMSCameraPosition.camera(withTarget: location.coordinate, zoom: 14)
}];
If you dont' want to use the above helper code, then you can get user's location using Core Location basic api as well.
Note that your code calls observeValue every time the user's location is changed, that results in setting camera for user's location map.
With help from Sunil's answer above, I figured out how to solve this problem.
Sunil notes that the app calls observeValue() every time the user's location is updated. So based off of the code I had in observeValue() this would make sense that the mapView camera would update every time the user's location updates.
I solved the problem by moving
self.mapView.camera = GMSCameraPosition.camera(withTarget: myLocation.coordinate, zoom: 14)
to another function like viewDidAppear().
Some may ask why I didn't move it to viewDidLoad() as that is called before viewDidAppear(). The app doesn't fetch the user's location until the end of viewDidLoad(). So putting the camera declaration at the end of the viewDidLoad() doesn't give the app enough time to fetch the users location. By declaring the camera position in the viewDidAppear() function, we give the app ample time to process the user's location and fetch it.
Note: Make sure to pass your location variable out of the observeValue() function for use in viewDidAppear().
In lack of a better title...
I'm am writing a custom turn-by-turn implementation in an app used for tracking as well as giving directions using CLLocation and MKMapView.
Getting a route for a given address, displaying it with the returned polyline, getting an ETA and a distance and displaying those all seems fairly trivial and easily implemented. One thing that there isn't a lot of guidance on though, is how to display MKRouteStep's returned in the MKRoute object. All guides online either don't bother mentioning them, or skip over them fairly easily by simply displaying them at once in a table view. Obviously not the most gracious solution, and in my opinion a critical part that's missing.
One solution I did find (can't find the SO answer I saw it in) mentioned creating a CLRegion for each step. So I wen't ahead and created my own implementation of it:
private func drawRouteOnMap() {
viewModel.getRouteForDelivery() { route in
if let route = route {
self.polyline = route.polyline
self.mapView.addOverlay(route.polyline, level: .AboveRoads)
//Add the route steps to the currentRouteSteps array for later retrieval
self.currentRouteSteps = route.steps
//Set the routeStepLabels text property to the first step of the route.
if let step = route.steps.first {
self.routeStepLabel.text = step.instructions
}
//Iterate over the steps in the `MKRoute` object. Create each region to monitor with the identifier set to an Int
var i = 0
for step in route.steps {
let coord = step.polyline.coordinate
let region = CLCircularRegion(center: coord, radius: 20.0, identifier: "\(i)")
self.locationManager.startMonitoringForRegion(region)
i += 1
}
}
}
}
Then I can listen to the func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {} delegate method
private func updateUIForRegion(oldRegion:CLRegion) {
if let regionID = Int(oldRegion.identifier),
steps = currentRouteSteps {
//Fetch the step for the next region
let step = steps[regionID + 1]
routeStepLabel.text = step.instructions
}
}
Then when we exit the view controller
#IBAction func shouldDismissDidPress(sender: AnyObject) {
for (_, region) in locationManager.monitoredRegions.enumerate() {
locationManager.stopMonitoringForRegion(region)
}
dismissViewControllerAnimated(true, completion: nil)
}
This all works fairly well actually. But where things start to fall apart is if a user presses the home button when in the turn by turn navigation VC or if the app crashes. The app will not stop monitoring the regions set up until the locationManager is told to stop monitoring for them, even if the app is force closed. I wouldn't want the app to terminate all the region monitoring anyways, as the navigation wouldn't work when the user resumes the app. It just feels like the region monitoring isn't really the correct way to display the MKRouteStep's, but I'm out of ideas how to do it in another way.
Any ideas / better implementations are welcomed
So, first, the way that works is by definition the most elegant way - so if you have some code that handles every possibility, then by all means go for it!
With that in mind, for cases where there isn't a frank crash but your app is closed could you use one of the many delegate calls in AppDelegate to cease your monitoring?
I've been working in a iOS location tracking application, and we've found a way to determine when users are leaving a place, but we're doing it by constantly listening to location updates, which ends draining our battery.
What is the most efficient way to do a thing like this? i would like to get a battery consumption similar to the reminders application.
Any idea is welcome!
Thanks!
You should set up your app to use geofences.
The method you want to look at is the CLLocationManager method startMonitoringForRegion. You need to set up your app to ask the user for permission to monitor location updates.
The system will launch your app to deliver a region update if it's not running.
It depends on your definition of "leaving a place". But you can use
func startMonitoringSignificantLocationChanges()
On CLLocationManager in order to be notified (roughly) when the user moves 500 meters or more, according to Apple documention.
This shifts the responsibility of watching battery life over to Apple's code, which as you might imagine is well optimized for the task.
Follow #rschmidt's function for starting location update on device.
Track user's current locality by keeping a variable named 'currentLocality' in the class which is delegating CLLocationManager.
Implement the CLLocationManagerDelegate in the following way
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
var currentLocation = locations.last as? CLLocation
// Getting current locality with GeoCoder
CLGeocoder().reverseGeocodeLocation(currentLocation, completionHandler: {(placemarks, error) in
if (error != nil) {
println("reverse geodcode fail: \(error.localizedDescription)")
}
else {
let placeMark = placemarks.last as? CLPlacemark
if let latestLocality = placeMark!.locality {
if latestLocality != currentLocality {
// Locality has been changed, do necessary actions
// Updating current locality
currentLocality = latestLocality
}
}
}
})
}
I currently have a test application to teach myself Core Location. It is an app with a button which will either show your location when you press a button, or an alert when location services are off.
I get a fatal error (unexpectedly found nil when unwrapping option value) When location services are on and I attempt to println the string. I cannot find anywhere in the documentation where the CLLocationManager.location.coordinate returns an optional value. I will include the code snippet. Thanks!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
if CLLocationManager.authorizationStatus() == .NotDetermined {
locationManager.requestWhenInUseAuthorization()
}
}
#IBAction func testLocation(sender: AnyObject) {
switch CLLocationManager.authorizationStatus() {
case .AuthorizedAlways, .AuthorizedWhenInUse:
printCurrentCoordinates()
case .Restricted, .Denied:
disabledLocationSeriveAlert()
default:
println("Failed")
}
func printCurrentCoordinates() {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
println("\(locationManager.location.coordinate.latitude)")
println("\(locationManager.location.coordinate.longitude)")
}
func disabledLocationSeriveAlert() {
let alert = UIAlertController(title: "Unable to retrieve location", message: "Enable location services in system settings", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
It looks like you aren't giving the CLLocationManager enough time to update the location.
I looked at the documentation, and in the statement locationManager.location.coordinate.latitude, location is the only one that's an optional. It's implicitly unwrapped, so that's why you don't need an ! after it.
In the printCurrentCoordinates method, you attempt to print the coordinates immediately after you call startUpdatingLocation. The documentation for location says:
The value of this property is nil if no location data has ever been retrieved.
And the documentation for startUpdatingLocation says:
This method returns immediately. Calling this method causes the location manager to obtain an initial location fix (which may take several seconds) and notify your delegate by calling its locationManager:didUpdateLocations: method.
So, you aren't giving the location manager enough time to get a location fix before you attempt to print the location.
You have a couple of choices - you can call startUpdatingLocation earlier and then print the coordinates in printCurrentCoordinates, or you could have printCurrentCoordinates start updating the location and then print the coordinates in locationManager:didUpdateLocations:.
The issue is location property of locationManager will be nil at that time.
That property contains the most recently retrieved location data. You just allocated location manager, at that time there won't be any data in that property.
According to CLLocationManager Class Reference:
location
Property The most recently retrieved user location.
(read-only)
Declaration
#NSCopying var location: CLLocation! { get }
Discussion
The value of this property is nil if no location data has
ever been retrieved.
location is nil when not set (from doc)
The value of this property is nil if no location data has ever been retrieved.
You have to use delegate to get the location. This is asynchronous operation, so value not available right after call startUpdatingLocation()
You must implement CLLocationManagerDelegate (and set your object as delegate) and wait for locationManager:didUpdateLocations: method - only then location will have some data.
Obtaining the location is an asynchronous operation - in fact the method you invoke to start the op is startUpdatingLocation() and not getLocation(). That is an indication that the process is started, and it's not completed at the time the invoked method returns.
The location is provided asynchronously via the func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) method, part of the CLLocationManagerDelegate protocol that you must adopt in your class. And of course you have to set the delegate property of the CLLocationManager instance. If you have ever worked with tables, you should know how it works.
However, I recommend reading the official docs, or a good tutorial like this one