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().
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
Please check the code:
let manager = CLLocationManager()
//Location manager to determine the current location
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations[0]
lat = location.coordinate.latitude
lon = location.coordinate.longitude
let currentLocation = CLLocation(latitude: lat!, longitude: lon!)
}
And i have few more functions in viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestWhenInUseAuthorization()
//Here i want to get the result immediately, but...
manager.startUpdatingLocation()
parseJSONfunction()
anotherFunction()
anotherFunction2()
...}
So, i can't get the result of manager.startUpdatingLocation() function until all viewDidLoad() stuff loads.
The question: Is it possible to get coordinates before all other functions runs? If yes, please describe how?
The question: Is it possible to get coordinates before all other functions runs?
In a word, no. The location manager is asynchronous. You ask it to start updating your location and it fires up the GPS and tries to get a fix (it also uses cell towers, WiFi base stations, etc.) It can take several seconds (or longer) in order to get a reasonably accurate reading.
When I'm writing location-aware apps, I usually start the location manager, and in my locationManager(_:didUpdateLocations:) method I check the horizontal accuracy of the result and only take it if it's at least reasonably accurate. This can make it take even longer.
If you load a viewController it's synchronous. The system does the setup, which causes various framework calls to fire, and then viewDidLoad gets called, synchronously once the view loads. If you start the location manager when the user asks to display a new view controller, you can't possibly have a location fix by the time viewDidLoad is called.
If you load your app, have the app delegate invoke a singleton to begin location updates, and then wait for the user to switch to your other screen, where you then ask the singleton for the location during viewDidLoad you have a good chance of getting a good location reading, but even then it isn't certain.
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.
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
}
}
}
})
}
So, my app requires to capture location every 15 meter.
in viewDidLoad
NSOperationQueue.mainQueue().addOperationWithBlock {
self.manager = CLLocationManager()
self.manager.delegate = self
self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
self.manager.distanceFilter = 15.0
self.manager.headingFilter = 10
self.manager.startUpdatingLocation()
}
First time (i don't move), in
func locationManager(manager:CLLocationManager, didUpdateLocations locations:[AnyObject])
i get (real example):
*.1488241293991
*.5997807433053
second time:
*.1489010891664
*.599624152471
I test on real device with ios7, and swift.
What am I doing wrong?
Thanks!
From CLLocationManager Class Reference:
When requesting high-accuracy location data, the initial event delivered by the location service may not have the accuracy you requested. The location service delivers the initial event as quickly as possible. It then continues to determine the location with the accuracy you requested and delivers additional events, as necessary, when that data is available.
So the first location you retrieve might be inaccurate.