NSMangedObject subclass method called constantly after changing NSFetchedResultsController NSPredicate - ios

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.

Related

iOS completion handler on geocodeAddressString(String:) not executing statements

Currently making an app in which a view controller takes in the addressString and then geocodes the address to retrieve the latitude and longitude of said address as seen below inside of a #objc method.
#objc func addLocationTouchUpInside() {
let addressString = "\(streetAddressField.text!), \(cityField.text!), \(stateField.text!)"
var location = Location(id: Location.setid(), locationName: nil, addressString: addressString, latitude: nil, longitude: nil, HoHid: nil)
// This geocoder instance is a let property initialized with the class
geocoder.geocodeAddressString(location.addressString) { placemark, error in // Completion handler starts here...
print("WHY")
if error != nil {
print("error: \(error!)")
}
if let placemark = placemark?[0] {
if let addresslocation = placemark.location {
location.latitude = addresslocation.coordinate.latitude
location.longitude = addresslocation.coordinate.longitude
}
}
print("3: \(location.latitude)")
}
do {
try AppDatabase.shared.dbwriter.write({ db in
try location.insert(db)
})
self.currentUser?.locationid = location.id
try AppDatabase.shared.dbwriter.write({ db in
if var user = try User.fetchOne(db, key: self.currentUser?.id) {
user = self.currentUser!
try user.update(db)
}
})
} catch {
fatalError("\(error)")
}
let locationInputViewController = LocationInputViewController()
locationInputViewController.setUser(user: self.currentUser!)
locationInputViewController.setLocation(location: location)
locationInputViewController.setHoH()
if let navigationController = self.navigationController {
navigationController.pushViewController(locationInputViewController, animated: true)
}
}
The issue I am having is that any code within the completion handler (of type CLGeocodeCompletionHandler) does not seem to execute. Originally I had assumed that location simply wasnt being mutated for some reason. I then learned that nothing within the completion handler executes.
This question may seem like a duplicate to this post however it seems a clear answer was not quite found. Via some comments on said post I had set both set a custom location for the simulator and used my personal iphone and saw no difference.
EDIT: I have updated the question with the entire function as requested in the comments. Second the completion handler I have been referring to would be that of CLGeocodeCompletionHandler. Defined as
typealias CLGeocodeCompletionHandler = ([CLPlacemark]?, Error?) -> Void
where
func geocodeAddressString(_ addressString: String, completionHandler: #escaping CLGeocodeCompletionHandler)
in the documentation. I have also commented the line where the completion handler starts.
Finally in only ONE of my runs was this strange error outputted:
#NullIsland Received a latitude or longitude from getLocationForBundleID that was exactly zero

CLFloor returns level 2146959360?

I’m using CLLocationManager, looking at the location property and examining the level of the floor, if any. The documentation suggests that if it couldn’t determine the floor, that it would just return nil. In practice, I am getting a CLFloor instance, but its level is 2146959360. Converting that to hex, 0x7FF80000, which looks suspiciously like some cryptic sentinel value.
lazy var locationManager: CLLocationManager = {
let locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
return locationManager
}()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
switch CLLocationManager.authorizationStatus() {
case .notDetermined: locationManager.requestWhenInUseAuthorization()
case .denied: redirectToSettings()
default: break
}
}
#IBAction func didTapGetLocation(_ sender: Any) {
updateLabels(for: locationManager.location)
}
func updateLabels(for location: CLLocation?) {
guard let location = location else {
floorLabel.text = "No location."
return
}
if let floor = location.floor {
let hexString = "0x" + String(format: "%08x", floor.level)
floorLabel.text = "\(floor.level) (\(hexString))"
} else {
floorLabel.text = "No floor."
}
}
I’m seeing this behavior in on a physical iOS 13.3.1 devices, only. FWIW, older iOS versions (I’ve only got iOS 10 device sitting here) appear to return nil, as expected, as does the simulator.
What’s going on?
This problem goes away if you call startUpdatingLocation. If you do that, then the floor property will be nil. This CLFloor instance with a level value of 2146959360 (0x7FF80000) only appears if you query the location of the CLLocationManager without having first called startUpdatingLocation.
The documentation suggests that this location property is populated with the last known value. Regardless, the floor should be nil (for my particular location, at least) but isn’t. The level is invalid.
See this repo for example of manifestation of the bug and demonstration of how calling startUpdatingLocation works.
I’ve filed a bug report (FB7638281).

location in the background does not work

I am developing a code that should get my location every 10 minutes and salvation in CoreData. When I walk into background with conectavo app to xcode can not see the log that the service is running, but when I go out walking the streets he simply not saved or saved too few times.
This is part of my code that should do this function.See save in codeData:
var saveLocationInterval = 60.0
func applicationDidEnterBackground(application: UIApplication) {
UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler(nil)
self.timer = NSTimer.scheduledTimerWithTimeInterval(saveLocationInterval, target: self, selector: #selector(AppDelegate.saveLocation), userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(self.timer, forMode: NSRunLoopCommonModes)
locationController.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
saveLocation()
}
func saveLocation(){
print("======")
let logUser = NSEntityDescription.insertNewObjectForEntityForName("LOG_GPS", inManagedObjectContext: self.managedObjectContext) as! LOG_GPS
if locationController.location == nil{
logUser.latitude = ""
logUser.longitude = ""
} else {
logUser.latitude = "\(locationController.location!.coordinate.latitude)"
logUser.longitude = "\(locationController.location!.coordinate.longitude)"
}
logUser.velocidade = userSpeed > 0 ? Int(userSpeed) : 0
logUser.address = "\(userSpeed)"
if _usuario.chave != nil {
logUser.chave_usuario = "\(_usuario.chave!)"
}
if _empresa.chave != nil {
logUser.chave_licenca = "\(_empresa.chave!)"
}
print("localizaçao salva no bd \(logUser.latitude)")
let date = NSDate()
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "dd/MM/yy HH:mm:ss"
let dateString = dateFormatter.stringFromDate(date)
logUser.data = dateString
do {
try self.managedObjectContext.save()
} catch {
}
}
Another major error in my code I can not solve is the User's speed. In the method the low I'm trying to save your speed in a variable and then save the CoreData however this is me always returning a negative value:
func locationManager(manager: CLLocationManager, didUpdateToLocation newLocation: CLLocation, fromLocation oldLocation: CLLocation) {
var speed: CLLocationSpeed = CLLocationSpeed()
speed = newLocation.speed
print(speed * 3.6)
userSpeed = speed * 3.6
}
this is my background mode
Possible solution 1
You need a key in your Info.plist that describes why your app needs background location.
Go to your Info.plist, find the Bundle Version key and click the + that appears when you hover over that. Then add the key NSLocationAlwaysUsageDescription, set it to be a string, and set the value as whatever you want the description to be, like "We need your location in the background so we share your location with friends."
Now your app should work. If it doesn't....
Possible solution 2 (more likely solution if you know what you're doing)
With iOS 9, Apple made it so that apps on physical devices need a special line of code to run location services in the background. The change was not widely reported on (if at all?) but I managed to figure this one out a while ago. Here's what you need to do to get location services working in the background on physical devices again:
In your main location tracking view controller's ViewDidLoad put...
if #available(iOS 9.0, *) {
locationManager.allowsBackgroundLocationUpdates = true
} else {
// You don't need anything else on earlier versions.
}
This will (mysteriously enough) likely be all you need to solve your problem.

SWIFT - LocationManager looping through multiple times?

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)

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