SWIFT - LocationManager looping through multiple times? - ios

I have a locationManager function to grab the users current location and posting the name of the city and state. I have a print statement so I can check in my console if everything is working properly...and it is. However, it prints the city location 3 times. This actually causes an issue in my actual app but thats beyond the point of this question.
My function is as follows:
var usersLocation: String!
var locationManager: CLLocationManager!
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let userLocation: CLLocation = locations[0]
CLGeocoder().reverseGeocodeLocation(userLocation) { (placemarks, error) -> Void in
if error != nil {
print(error)
} else {
let p = placemarks?.first // ".first" returns the first element in the collection, or nil if its empty
// this code above will equal the first element in the placemarks array
let city = p?.locality != nil ? p?.locality : ""
let state = p?.administrativeArea != nil ? p?.administrativeArea : ""
self.navigationBar.title = ("\(city!), \(state!)")
self.usersLocation = ("\(city!), \(state!)")
self.locationManager.stopUpdatingLocation()
print(self.usersLocation)
self.refreshPosts()
}
}
}
So in my print(self.usersLocation) it will print in my console three times. Is this normal?
UPDATE TO SHOW VIEWDIDLOAD
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 250.0
}

I'd first suggest a few things:
Call stopUpdatingLocation before you perform reverseGeocodeLocation.
You are calling stopUpdatingLocation inside the reverseGeocodeLocation completion handler closure. The problem is that this runs asynchronously, and thus didUpdateLocations may receive additional location updates in the intervening period. And often, when you first start location services, you'll get a number of updates, often with increasing accuracy (e.g. horizontalAccuracy values that are smaller and smaller). If you turn off location services before initiating asynchronous geocode request, you'll minimize this issue.
You can also add add a distanceFilter in viewDidLoad, which will minimize redundant calls to the delegate method:
locationManager.distanceFilter = 1000
You can use your own state variable that checks to see if the reverse geocode process has been initiated. For example:
private var didPerformGeocode = false
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// if we don't have a valid location, exit
guard let location = locations.first where location.horizontalAccuracy >= 0 else { return }
// or if we have already searched, return
guard !didPerformGeocode else { return }
// otherwise, update state variable, stop location services and start geocode
didPerformGeocode = true
locationManager.stopUpdatingLocation()
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
let placemark = placemarks?.first
// if there's an error or no placemark, then exit
guard error == nil && placemark != nil else {
print(error)
return
}
let city = placemark?.locality ?? ""
let state = placemark?.administrativeArea ?? ""
self.navigationBar.title = ("\(city), \(state)")
self.usersLocation = ("\(city), \(state)")
print(self.usersLocation)
self.refreshPosts()
}
}

I had the same problem and Rob's answer didn't do it for me.
When the location service first starts, the location is updated multiple times regardless of the distanceFilter.
You might still want the location to be updated and you don't want to lose the location accuracy(which is the whole point of updating location multiple times on start-up), so calling stopUpdatingLocation(or using a local variable) after the first geolocating call isn't the way to go either.
The most intuitive way is to wrap your geocode call in an #objc function and call the the function with a delay:
NSObject.cancelPreviousPerformRequests(withTarget: self)
perform(#selector(myGeocodeFunction(_:)), with: location, afterDelay: 0.5)

Related

CLLocation distance tracking starts off incorrectly

I am new to Swift (and this website, so sorry if I am doing anything wrong), and I am trying to make a running app that tracks the user's location. While the function I used to track the distance works, it doesn't start at 0. When I hit the start button, the distance starts at a random number and then it starts tracking from there.
My question is: Is there something I am not addressing something correctly? If so, is there a way to fix it so that the tracking is more accurate? Here is what I have so far:
override func viewDidLoad() {
super.viewDidLoad()
stopwatchLabel.text = "00:00.00"
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
locationManager.activityType = .fitness
locationManager.distanceFilter = 10.0
mapView.showsUserLocation = true
startLocation = nil
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Location Delegate Methods
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
let location = locations.last
let center = CLLocationCoordinate2D(latitude: location!.coordinate.latitude, longitude: location!.coordinate.longitude)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.002, longitudeDelta: 0.002))
self.mapView.setRegion(region, animated: true)
if startLocation == nil {
startLocation = locations.first
}
var distance = startLocation.distance(from: location!)
let lastDistance = location?.distance(from: location!)
distance += lastDistance!
distanceString = "\(distance)"
distanceLabel.text = distanceString
}
Here is what the app looks like:
the run screen
I realize that other people have asked similar questions, but the questions either have no answer, or they are in a different language (such as Objective-C). If this question has been answered before and I'm just overlooking it, could someone please link the answer to me? Thank you!
When the location manager starts, the first location returned is the cached, last know location. You need to check for this, via the timestamp, as well as check for the level of accuracy that is returned. Something like this in your didUpdateLocations delegate:
let newLocation = locations.last
let timeDiff = newLocation?.timestamp.timeIntervalSinceNow
let accuracyNeeded:CLLocationAccuracy=100.0
if timeDiff < 5.0 && (newLocation?.horizontalAccuracy)!<=accuracyNeeded{
//your code here
}
You have to allow the sensors time to warm up.
Here is a typical didUpdateLocations implementation. We keep track of both the time elapsed since we started updating locations and the improving horizontal accuracy as the sensors warm up. If the horizontal accuracy doesn't improve in a reasonable time, we give up.
You will need a nil property, a Date?, called startTime, and constants REQ_TIME and REQ_ACC. self.stopTrying() turns off updates and resets startTime to nil.
let loc = locations.last!
let acc = loc.horizontalAccuracy
let time = loc.timestamp
let coord = loc.coordinate
if self.startTime == nil { // Date? property, keep track of time
self.startTime = Date()
return // ignore first attempt
}
let elapsed = time.timeIntervalSince(self.startTime)
if elapsed > REQ_TIME { // required time before giving up
self.stopTrying()
return
}
if acc < 0 || acc > REQ_ACC { // desired accuracy
return // wait for the next one
}
// got it
print("You are at \(coord.latitude) \(coord.longitude)")

App cancels with fatal error: unexpectedly found nil while unwrapping an Optional value

I'm designing myCurrentLocationBtn and got an error at let center when I switching off geolocation
#IBAction func CurrentLocation(sender: AnyObject) {
let Authflag = CLAuthorizationStatus.self
print(Authflag)
let userLocation = locationmanager.location
let center = CLLocationCoordinate2D(latitude: userLocation!.coordinate.latitude, longitude: userLocation!.coordinate.longitude)
var currentAnnotation = MKPointAnnotation()
currentAnnotation.coordinate = center
currentAnnotation.title = "You're here!"
MapApple.addAnnotation(currentAnnotation)
}
The error is fatal error: unexpectedly found nil while unwrapping an Optional value
How can I fix it?
With disabled geolocation such function
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
locationmanager.stopUpdatingLocation()
print (error)
}
error this Error Domain=kCLErrorDomain Code=0 "(null)"
Just use guard to make sure it's not nil:
#IBAction func CurrentLocation(sender: AnyObject) {
let Authflag = CLAuthorizationStatus.self
print(Authflag)
guard let userLocation = locationmanager.location else {
//tell the user that an error occurred
return
}
let center = CLLocationCoordinate2D(latitude: userLocation.coordinate.latitude, longitude: userLocation.coordinate.longitude)
var currentAnnotation = MKPointAnnotation()
currentAnnotation.coordinate = center
currentAnnotation.title = "You're here!"
MapApple.addAnnotation(currentAnnotation)
}
In Apple docs location property has this note:
The value of this property is nil if no location data has ever been retrieved.
So the problem is that when location services are disabled, your CLLocationManager doesn't have any location, and he returns nil. And when you are trying to create CLLocationCoordinate2D for center, you are force-unwrapping nil value:
userLocation!.coordinate.latitude
One of the possible solutions to this is to check that you actually get any location:
if let userLocation = locationmanager.location {
...
} else {
...
}
and instead of creating MKPointAnnotation do nothing, or show some UI that will notify user that he need to enable location services in order to see his position on the map.
Also take look at +[CLLocationManager locationServicesEnabled] method:
You should check the return value of this method before starting location updates to determine whether the user has location services enabled for the current device. Location services prompts users the first time they attempt to use location-related information in an app but does not prompt for subsequent attempts. If the user denies the use of location services and you attempt to start location updates anyway, the location manager reports an error to its delegate.

NSMangedObject subclass method called constantly after changing NSFetchedResultsController NSPredicate

I've been stuck with this and cannot find the culprit. I will try to include as much useful code that I can. See bottom for TL;DR version.
IDEA
User opens the app, you can find restaurants in a certain city OR set it to find the restaurants near your location (assuming you allowed the app to see your location). If you set the city, then the NSFetchedResultsController performs a fetch with a certain NSPredicate. When you tap on the button to find the restaurants near you, it changed the NSPredicate and executes the fetch again.
SET UP
1) When my UIViewController is loaded, it called a method on a subclass of NSUserDefaults to grab the user's current location.
2) This method tells a CLLocationManager object to start updating the user's location, once it has updated the location, the didUpdateLocations method gets called.
3) Since I have .desiredAccuracy set to kCLLocationAccuracyBest the didUpdateLocations method gets called often, so I set a timer to perform a method called updateRestaurantDistance which performs a fetch on the main NSManagedObjectContext and retrieves all the restaurants in the database.
4) A loop iterates through each restaurant and calls a method to set the restaurant's distance from the user.
5) Once the loop is finished, I save the context with the updated values.
ISSUE
Everything runs fine when I start the app, and once the fetch from 4) is finished iterating through each item, I can tap the button that fetches the restaurants that are near me.
However, if the iterations from 4) aren't finished, and I tap the button that finds the restaurants near me, the iterations finish, but the context is not saved (I have print statements set up) and the method that updates the restaurant distance is called infinitely blocking the main thread and making the UI unusable.
CODE
- UIViewController
override func viewDidLoad() {
super.viewDidLoad()
//configure properties
//core data
context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
//fetch controller
fetchController.delegate = self
//default values
user = User()
self.user.getCurrentLocation()
}
//method that is called when the button to filter restaurants near user is tapped
func nearMe() {
restaurantPredicate = NSPredicate(format: "(distance < %f AND distance > %f) AND SUBQUERY(specials,$s, ($s.type LIKE %# OR $s.type LIKE %#) AND $s.day LIKE %#).#count > 0",
user.doubleForKey(UserData.MaxDistance.rawValue), 0.01, typeQuery!, "Combo", day)
fetchController.fetchRequest.sortDescriptors = [distSort,nameSort,citySort]
do {
try fetchController.performFetch()
} catch {
print("Error fetching when sorting by near me")
}
tableView.reloadData()
}
The User class that has the method to get location
func getCurrentLocation() {
//ask for access to location (if not given)
if CLLocationManager.locationServicesEnabled() {
locationManager.requestWhenInUseAuthorization()
}
//if allowed, start checking for location
if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
}
The CLLocationDelegate
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// get coordintes
if let locValue: CLLocationCoordinate2D = manager.location?.coordinate {
//check if timer has started
if timer == nil {
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "updateRestaurantDistance", userInfo: nil, repeats: false)
} else {
timer?.invalidate()
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "updateRestaurantDistance", userInfo: nil, repeats: false)
}
//stop updating since we're done
locationManager.stopUpdatingLocation()
}
}
Update location method
func updateRestaurantDistance() {
let fetch = NSFetchRequest(entityName: "Restaurant")
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
do {
let results = try context.executeFetchRequest(fetch) as! [Restaurant]
for result in results {
print(results.indexOf(result))
result.getDistance()
}
} catch {
print("Error updating restaurant distances from UserClass")
}
do {
try context.save()
print("saved context")
} catch {
print("error saving context while updating restaurant distance: \(error)")
}
}
The Restaurant methods
//This method gets called infinitely and I have no idea why
func getDistance() {
print("getDistance")
guard CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse else {
return
}
self.distance = self.getDistanceFromRestaurant()
}
func getDistanceFromRestaurant() -> CLLocationDistance {
let user = User()
guard CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse else {
return Double(0)
}
let longitude = user.doubleForKey(UserData.Longitute.rawValue)
let latitude = user.doubleForKey(UserData.Latitude.rawValue)
guard abs(longitude) + abs(latitude) > 0 else {
return Double(0)
}
let loc = CLLocation(latitude: latitude, longitude: longitude)
let selfLoc = CLLocation(latitude: Double(self.latitude), longitude: Double(self.longitude))
return loc.distanceFromLocation(selfLoc) / 1000
}
Happy to add more if needed, thank you!
EDIT: Seems that it gets to try context.save() in updateRestaurantDistance() and starts the loop there (aka it doesn't get to the print statement
EDIT 2: TL;DR Code version:
This updates the restaurant's distance to the user, trys are in do-catch blocks but removed for simplicity
func updateRestaurantDistance() {
let fetch = NSFetchRequest(entityName: "Restaurant")
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let results = try context.executeFetchRequest(fetch) as! [Restaurant]
for result in results {
result.getDistance()
}
try context.save()
}
func getDistance() {
self.distance = 10 // method to calculate distance is called, but for simplicity set to 10
}
While updateResturantDistance is running, I cannot change the NSPredicate to anything with a distance attribute and perform a fetch for my NSFetchedResultsController i.e:
predicate = NSPredicate(format: "distance < %f", 10)
fetchController.fetchRequest.predicate = predicate
try fetchController.performFetch
doesn't work, but
predicate = NSPredicate(format: "city LIKE %#", "New York")
fetchController.fetchRequest.predicate = predicate
try fetchController.performFetch
works
Either
disable the UI control that causes the reload until everything is finished
or
keep a Bool variable around indicating if you are looping through the locations. When the control is hit, change this variable. In the loop check the variable and abort if it has changed and start from scratch without saving.
OH. MY. GOODNESS.
WOW
So let's recall getters and setters. In most programming languages that aren't like swift i.e Java, you must create them with methods i.e
private var distance = 1
getDistance() -> Int {
return self.distance
}
But in swift, this isn't necessary. Anyways, turns out my getDistance method was being called for my NSPredicate. I changed the name of the method to getRestaurantDistance and everything runs as expected. Do not be like my and name methods that would also be named the getter and setter.

Standard Location based iOS app not waking up after suspended IOS

I am developing a location based app which is supposed to fetch user location always.Im using standard location service. But the problem is that the app after keeping idle for some time in background will not fetch the coordinates even after we move to some other locations. As per apple documentation, when a new location arrives, app should wake up automatically, but that is not happening here. I'm sharing the code and using to fetch location and screenshot of my plist.
class SALocation: NSObject,CLLocationManagerDelegate
{
static let sharedInstance : SALocation = SALocation()
var locationManager : CLLocationManager!
var location : CLLocation!
var address : String!
var latitude : NSString?
var longitude : NSString?
var isAdderssLoaded : Bool = false
var locdictionary : NSMutableDictionary = NSMutableDictionary()
func startLocationManager()
{
if self.locationManager == nil
{
self.locationManager = CLLocationManager()
if CLLocationManager.locationServicesEnabled(){
print("location service enabled")
}
self.locationManager.delegate = self
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.distanceFilter = kCLDistanceFilterNone
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
if ( Float (UIDevice.currentDevice().systemVersion) >= 9) {
if #available(iOS 9.0, *) {
self.locationManager.allowsBackgroundLocationUpdates = true
} else {
// Fallback on earlier versions
};
}
self.locationManager.startUpdatingLocation()
//self.locationManager.stopMonitoringSignificantLocationChanges()
}
else
{
self.locationManager.startUpdatingLocation()
}
}
// MARK: CLLocationManagerDelegate
func locationManager(manager: CLLocationManager, didFailWithError error: NSError)
{
UIAlertView(title:"Alert", message:error.description, delegate: nil, cancelButtonTitle:nil, otherButtonTitles:"Ok").show()
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
if locations.count > 0
{
self.location = locations[0]
/* storing date and location to plist
*/
let datenow = NSDate()
let dateformatternow = NSDateFormatter ()
dateformatternow.dateFormat = "yyyyMMdd HH:mm:ss"
let timenow:NSString = dateformatternow.stringFromDate(datenow)
let documetsdirectorypath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last
latitude = NSString(format: "%f",self.location.coordinate.latitude)
longitude = NSString (format: "%f",self.location.coordinate.longitude)
let latlong : NSString = NSString(format:"%#~%#",latitude!,longitude!)
NSUserDefaults.standardUserDefaults().setObject(latlong, forKey: "latlong")
let aFilePath = NSString(format: "%#/location.plist",documetsdirectorypath!)
locdictionary.setObject(latlong, forKey: timenow as String)
locdictionary.writeToFile(aFilePath as String, atomically: true)
///////////// ||storing date and location to plist code ends here||\\\\\\
// self.getAddressFromLocation(locations[0] )
// if (NSUserDefaults.standardUserDefaults().objectForKey(SettingAppRefresh) != nil)
// {
// if (NSUserDefaults.standardUserDefaults().objectForKey(SettingAppRefresh) as! NSString).isEqualToString(FalseString)
// {
// // self.locationManager.stopUpdatingLocation()
// }
// }
}
}
}
What i'm doing here is just get location and write it to a plist file. This works in foreground, background etc fine. But when i keep the app idle for 20 minutes, location is not fetched even if i move to some other locations as the app is suspended
Capabilities tab looks like this
To start location in background you must start background service from the following path
Click on your name -> Click on your app name (target) -> goto capabilities -> find the background mode -> enable the location update mode
I am not sure you started that or not because you not put any screenshot about this.
And also check that your user started background refresh in settings.refer below link for this.
Background App Refresh checking, enabling and disabling programatically for whole device and for each particular application in iOS 7
Update::
For location update in background used below link(objective c)
http://www.creativeworkline.com/2014/12/core-location-manager-ios-8-fetching-location-background/
Well, I don't know how you're getting location updates - significant-location change as example and how you exit from background.
I suggest checking if your app is truly in background mode - UIApplication.sharedApplication().applicationState as it can be terminated.
And I also suggest checking out Apple's Execution States for Apps. - especially for your possible use case Implementing Long-Running Tasks part. There is also a good tutorial at rayywenderlich.com called Background modes.
Please use
self.locationManager.requestAlwaysAuthorization()
and don't forget to update your Info.plist to define the NSLocationAlwaysUsageDescription key.

Swift CLLocation does not update location at startup

I have an application were I want to get the users location in viewDidLoad, I then store the lat and long in variables and use them in a function to get data based on the position from the user. I have a timer that calls a function every x minute that gets data (let´s call it getData()), the first time getData() is called lat and long is 0, but the second, third time etc.. they have values.
When I update the coords (during runtime) in the simulator I do get the updated values for both lat and long, but it´s the first run that lat and long always are 0. My code looks like this:
let locationManager = CLLocationManager()
var getDataGroup = dispatch_group_create()
override func viewDidLoad() {
super.viewDidLoad()
dispatch_group_enter(getDataGroup)
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
dispatch_group_leave(getDataGroup)
dispatch_group_wait(getDataGroup, DISPATCH_TIME_FOREVER)
dispatch_async(dispatch_get_main_queue()) {
self.getData()
}
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
CLGeocoder().reverseGeocodeLocation(manager.location, completionHandler: { (placemarks, error) -> Void in
if (error != nil){
println("Error: " + error.localizedDescription)
return
}
if (placemarks.count > 0){
let pm = placemarks[0] as! CLPlacemark
self.displayLocationInfo(pm)
}
else{
println("Error with location data")
}
})
}
func displayLocationInfo (placemark : CLPlacemark){
self.locationManager.stopUpdatingLocation()
lat = String(stringInterpolationSegment: placemark.location.coordinate.latitude)
long = String(stringInterpolationSegment: placemark.location.coordinate.longitude)
}
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
println("Error: " + error.localizedDescription)
}
func getData(){
// Do stuff with lat and long...
}
For some reason the first run lat and long are both 0, the rest of the runs they have a valid value. The problem is that getData is called before the locationManager and displayLocationInfo methods even though I have added self.locationManager.startUpdatingLocation() before the call to getData() for some reason.
I don't really understand what your code is supposed to do.
There's no reason for the GCD code in your viewDidLoad. simply create an instance of the location manager and tell it to start updating your location.
Then your didUpdateLocations method will be called once a location is available. Use the last location in the array of locations you get in that method - it will be the most recent.
I would advise checking the horizontalAccuracy of the locations before using them. Typically the first few location readings are really bad, and then slowly the readings settle down. The Horizontal accuracy reading is actually a radius giving the possible area of the reading. If you get 1 km, it means your location could be anywhere in a circle 1 km in radius. You probably want to discard readings that bad and wait for one that's within a few hundred meters (or better.)
Your viewDidLoad method is calling a method getDeparturesAtStop, and that method will likely be called before the first call to didUpdateLocations fires. (You don't show what that method does.)
Where is the code that is getting a zero lat/long?

Resources