The issue is that the code inside the completionHandler block is never run; I used breakpoints and the program would skip over the completion handler block in build mode
Below are two functions used within PinALandPlaceMark, where most of my code is located
func generateRandomWorldLatitude()-> Double{
let latitude = Double.random(in: -33 ..< 60)
return latitude
}
func generateRandomWorldLongitude()-> Double{
let longitude = Double.random(in: -180 ..< 180)
return longitude
}
func PinALandPlaceMark() -> MKAnnotation {
var LandBasedCountryHasYetToBeFound : (Bool, CLLocationDegrees?, CLLocationDegrees?)
LandBasedCountryHasYetToBeFound = (false,nil,nil)
let randomPinLocation = MKPointAnnotation()
repeat{
if LandBasedCountryHasYetToBeFound == (false,nil,nil){
let latitude: CLLocationDegrees = generateRandomWorldLatitude()
let longitude: CLLocationDegrees = generateRandomWorldLongitude()
let randomCoordinate = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(randomCoordinate, completionHandler: { (placemarks, error) -> Void in
if error != nil{print(error)}else{
guard let placemark = placemarks?.first else{return}
//check which placemark property exists, store in the 0 alpha labelForClosure
if let countryExists = placemark.country {
LandBasedCountryHasYetToBeFound = (true,latitude,longitude)
//country = countryExists as String
// viewController.labelForClosure.text = countryExists
print(" Country Exists!: \(countryExists)")
print(" randomCoordinate \(randomCoordinate)")
}
}
})
}
// print("The country found was on land. This statement is \(LandBasedCountryHasYetToBeFound.occursInCountry)")
else{
let coordinatesOfrandomPinLocation = CLLocationCoordinate2D(latitude: LandBasedCountryHasYetToBeFound.1!, longitude: LandBasedCountryHasYetToBeFound.2!)
randomPinLocation.title = ""//LandBasedCountryHasYetToBeFound.countryName
randomPinLocation.coordinate = coordinatesOfrandomPinLocation
// viewController.mapView.addAnnotation(randomPinLocation)
}
}while LandBasedCountryHasYetToBeFound.0 == false
print("randomPin has been returned, now use pin function inside placemark declaration")
return randomPinLocation
}
Your main problem is that your CLGeocoder instance is held in a local variable inside the loop; This means that it will be released before it has completed its task.
You have a couple of other issues too which would cause you problems even if the reverse geo-coding did complete.
The main one is that you are checking for loop termination using a boolean that is set inside the closure; The closure will execute asynchronously, so the loop will have executed many more times before the boolean is set to true in the case where an address is found.
The second problem is related to and made worse by this; reverse geocoding is rate limited. If you submit too many requests too quickly, Apple's servers will simply return an error. Even if you did wait for the first response before submitting a second, your chances of hitting land at random are pretty low, so you will probably hit this limit pretty quickly.
Ignoring the rate limit problem for the moment, you can use a recursive function that accepts a completion handler rather than using a loop and trying to return a value.
var geoCoder = CLGeocoder()
func pinALandPlaceMark(completion: #escaping (Result<MKAnnotation, Error>) -> Void) {
let latitude: CLLocationDegrees = generateRandomWorldLatitude()
let longitude: CLLocationDegrees = generateRandomWorldLongitude()
let randomCoordinate = CLLocation(latitude: latitude, longitude: longitude)
geocoder.reverseGeocodeLocation(randomCoordinate) { (placemarks, error) in
guard error == nil else {
completion(nil,error)
return error
}
if let placemark = placemarks.first, let _ = placemark.country {
let randomPinLocation = MKPointAnnotation()
randomPinLocation.coordinate = randomCoordinate.coordinate
completionHandler(randomPinLocation,nil)
} else {
pinALandPlaceMark(completion:completion)
}
}
}
The first thing we do is declare a property to hold the CLGeocoder instance so that it isn't released.
Next, this code checks to see if a placemark with a country was returned. If not then the function calls itself, passing the same completion handler, to try again. If an error occurs then the completion handler is called, passing the error
To use it, you would say something like this:
pinALandPlaceMark() { result in
switch result {
case .success(let placemark):
print("Found \(placemark)")
case .failure(let error):
print("An error occurred: \(error)")
}
}
Related
So sometimes someone in entered the search radius is from before, ie someone who was in search radius, but based on the current data in the database is not in the radius. Other times, someone who wasn't in the search radius before but now is, doesn't get printed.
This only happens once each time, ie if I load the app for the second time after the erroneous inclusion or exclusion, the correct array prints.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let databaseRef = Database.database().reference()
guard let uid = Auth.auth().currentUser?.uid else { return }
guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else { return }
print("locations = \(locValue.latitude) \(locValue.longitude)")
latestLocation = ["latitude" : locValue.latitude, "longitude" : locValue.longitude]
let lat = locValue.latitude
let lon = locValue.longitude
dict = CLLocation(latitude: lat, longitude: lon)
print("dict", dict)
if let locationDictionary = latestLocation {
databaseRef.child("people").child(uid).child("Coordinates").setValue(locationDictionary)
let geofireRef = Database.database().reference().child("Loc")
let geoFire = GeoFire(firebaseRef: geofireRef)
print(CLLocation(latitude: lat, longitude: lon),"GGG")
geoFire.setLocation(CLLocation(latitude: lat, longitude: lon), forKey: uid)
}
manager.stopUpdatingLocation()
}
Override func ViewdidLoad() {
super.viewDidLoad()
guard let uid = Auth.auth().currentUser?.uid else { return }
let geofireRef = Database.database().reference().child("Loc")
let geoFire = GeoFire(firebaseRef: geofireRef)
geoFire.getLocationForKey(uid) { (location, error) in
if (error != nil) {
print("An error occurred getting the location for \"Coordinates\": \(String(describing: error?.localizedDescription))")
} else if (location != nil) {
print("Location for \"Coordinates\" is [\(location?.coordinate.latitude), \(String(describing: location?.coordinate.longitude))]")
} else {
print("GeoFire does not contain a location for \"Coordinates\"")
}
}
let query1 = geoFire.query(at: self.dict, withRadius: 3)
query1.observe(.keyEntered, with: { key, location in
print("Key: " + key + "entered the search radius.") ///**this prints keys of users within 3 miles. This is where I see the wrong inclusions or exclusions**
do {
self.componentArray.append(key)
}
print(self.componentArray,"kr")
}
)
}
Here's what I would do for testing and maybe a solution. This is similar to your code but takes some of the unknowns out of the equation; I think we maybe running into an asynchronous issue as well, so give this a try.
In viewDidLoad get the current users position. That position will be used as the center point of the query
self.geoFire.getLocationForKey(uid) { (location, error) in
if (error != nil) {
print("An error occurred getting the location for \"Coordinates\": \(String(describing: error?.localizedDescription))")
} else if (location != nil) {
self.setupCircleQueryWith(center: location) //pass the known location
} else {
print("GeoFire does not contain a location for \"Coordinates\"")
}
}
Once the location var is populated within the closure (so you know it's valid) pass it to a function to generate the query
func setupCircleQueryWith(center: CLLLocation) {
var circleQuery = self.geoFire.queryAtLocation(center, withRadius: 3.0)
self.queryHandle = self.circleQuery.observe(.keyEntered, with: { key, location in
print("Key '\(key)' entered the search area and is at location '\(location)'")
self.myKeyArray.append(key)
})
}
self.queryHandle is a class var we can use to remove the query at a later time. I also set up self.geoFire as a class var that points to Loc.
EDIT
At the very top of your class, add a class var to store the keys
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
var ref: DatabaseReference!
var myKeyArray = [String]()
let queryHandle: DatabaseHandle!
and remember to also add a .keyExited event so you will know when to remove a key from the array when the key exits the area.
i'm trying to use swift geocoding to get the city, but somehow the city only showup nested inside the method and when returned the variable is empty, here is the code i'm using.
class {
var locationManager = CLLocationManager()
var longitude = CLLocationDegrees()
var latitude = CLLocationDegrees()
var city = ""
override func viewDidLoad() {
super.viewDidLoad()
setupLocation()
var x = getLocation()
print("\n\n x your city is: \(x)\n\n"); // 'x' is always empty
if x == "paris" {
print("\n\n your city is: \(x)\n\n"); // 'x' is always empty
}
}
func getLocation() -> String {
longitude = (locationManager.location?.coordinate.longitude)!
latitude = (locationManager.location?.coordinate.latitude)!
let location = CLLocation(latitude: latitude, longitude: longitude)
print(location)
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
print(location)
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
if placemarks!.count > 0 {
let pm = placemarks![0]
print("locality is \(pm.locality)")
self.city = pm.locality!
print(" city first \(self.city)") //contains a city
}
else {
print("Problem with the data received from geocoder")
}
})
print("city second \(city)") //empty every time
return city
}
}
As pointed out here, you have to add a completion handler to your method:
func getLocation(completion: #escaping (String) -> Void) {
longitude = (locationManager.location?.coordinate.longitude)!
latitude = (locationManager.location?.coordinate.latitude)!
let location = CLLocation(latitude: latitude, longitude: longitude)
print(location)
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
print(location)
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
if placemarks!.count > 0 {
let pm = placemarks![0]
print("locality is \(pm.locality)")
completion(pm.locality!)
}
else {
print("Problem with the data received from geocoder")
}
})
}
And then just do:
getLocation() {
locality in
self.city = locality
}
You have stumbled upon a time issue. reverseGeocodeLocation is asynchronous, so the and the method returns before the closure is fully evaluated.
If you set breakpoints you would see that the
print("city second \(city)") //empty every time
line would trigger before the
print(" city first \(self.city)") //contains a city
one
Problem:
reverseGeocodeLocation is an asynchronous method (it doesn't evaluate immediately and would take time to evaluate). Before reverseGeocodeLocation is completed getLocation will be completed.
Solution:
Modify the getLocation to accept a closure as a parameter. Inside the completion handler of reverseGeocodeLocation call that closure and pass that value of the city
I'm getting a BAD_EXC_ACCESS on line . The reason is "Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior".
func drawLocations(loc: CLLocation)
{
let center = CLLocationCoordinate2D(latitude: loc.coordinate.latitude, longitude: loc.coordinate.longitude)
let lat: CLLocationDegrees = center.latitude
let long: CLLocationDegrees = center.longitude
var points = [CLLocationCoordinate2DMake(lat,long),CLLocationCoordinate2DMake(lat,long),CLLocationCoordinate2DMake(lat,long),CLLocationCoordinate2DMake(lat,long)]
let polygon = MKPolygon(coordinates: &points, count: points.count)
mapView.addOverlay(polygon)//where I get error
}
func loadLocation(completion: (error:NSError?, records:[CKRecord]?) -> Void)
{
let query = CKQuery(recordType: "Location", predicate: NSPredicate(value: true))
CKContainer.defaultContainer().publicCloudDatabase.performQuery(query, inZoneWithID: nil){
(records, error) in
if error != nil {
print("error fetching locations: \(error)")
completion(error: error, records: nil)
} else {
print("found locations: \(records)")
print("found locations")
completion(error: nil, records: records)
guard let records = records else {
return
}
for(var i = 0; i<records.count; i += 1)
{
self.drawLocations(records[i]["location"] as! CLLocation)//where I call function
}
}
}
}
The completion block of performQuery "must be capable of running on any thread of the app" (as described in the docs). You call addOverlay which is a UI function, and so much be called on the main queue. You need to dispatch this method to the main queue.
Side note, unrelated to the question: for(var i = 0; i<records.count; i += 1) is much better written as for record in records. The C-style syntax is deprecated.
func getLatsAndLongs() -> (LATITUDE: Double, LONGITUDE: Double, DESIREDLAT: Double, DESIREDLONG: Double) {
self.forwardGeocoding("\(addressTxtFld.text) \(cityTxtFld.text), \(stateTxtFld.text)", completion: {
success, coordinate in
if success {
self.lat = coordinate.latitude
self.long = coordinate.longitude
print("\(self.lat) is the latitude for the initial location")
print("\(self.long) is the longitude for the initial location")
self.INITIAL_DESTINATION_LATITUDE = self.lat
self.INITIAL_DESTINATION_LONGITUDE = self.long
var initialLocation = CLLocationCoordinate2DMake(self.INITIAL_DESTINATION_LATITUDE, self.INITIAL_DESTINATION_LONGITUDE)
} else {
print("Error at forwardGeocoding #willcohen #ERRORALERT")
}
})
self.forwardGeocodingDesired("\(addressTxtFldDest.text) \(cityTxtFldDest.text), \(stateTxtFldDest.text)", completion: {
success, coordinate in
if success {
self.desiredLat = coordinate.latitude
self.desiredLong = coordinate.longitude
print("\(self.desiredLat) is the latitude for the desired location")
print("\(self.desiredLong) is the longitude for the desired locaiton")
self.DESIRED_DESTIANTION_LATITUDE = self.desiredLat
self.DESIRED_DESTINATION_LONGITUDE = self.desiredLong
var desiredLocation = CLLocationCoordinate2DMake(self.DESIRED_DESTIANTION_LATITUDE, self.DESIRED_DESTINATION_LONGITUDE)
} else {
print("Error at forwardGeocodingDesired #willcohen #ERRORALERT")
}
})
return (lat,long,desiredLat,desiredLong)
}
let latsAndLongs = getLatsAndLongs()
let latFF = latsAndLongs.LATITUDE
let longFF = latsAndLongs.LONGITUDE
let latDFF = latsAndLongs.DESIREDLAT
let longDFF = latsAndLongs.DESIREDLONG
print("\(latFF) final lat")
print("\(longFF) final long")
print("\(latDFF) final latD")
print("\(longDFF) final longD")
Okay. So, when I try to print all of the lines on the last 4 lines, it comes out as "0" each time. Note, the two geocoding lines (self.forwardGeocoding) & (self.forwardGeocodingDesired) are not the problems, they work fine, but I have no idea why they are not printing the correct Double values. Any suggestions would be greatly appreciated, thank you.
forwardGeocoding and forwardGeocodingDesired invoke an operations over the network which takes time to executes. This is done using a background thread so as not to block the UI. When the operations complete the code in the completion closures is executed.
Your getLatsAndLongs function returns lat,long,desiredLat and desiredLong before the operations have completed and therefore before these variables have been set by your closure.
You can pass a completion handler to getLatsAndLongs to be invoked after the operations are complete, but your situation is complicated because you are executing two geocoding operations in parallel, and you don't know which will finish first. You could dispatch the second one from the completion handler of the first, but this will be slower. A better approach is to use a dispatch_group:
func getLatsAndLongs(completion:(initialLocation: CLLocationCoordinate2D?, desiredLocation: CLLocationCoordinate2D?)->Void) {
let dispatchGroup = dispatch_group_create()
var initialLocation: CLLocationCoordinate2D?
var desiredLocation: CLLocationCoordinate2D?
dispatch_group_enter(dispatchGroup)
self.forwardGeocoding("\(addressTxtFld.text) \(cityTxtFld.text), \(stateTxtFld.text)", completion: {
success, coordinate in
if success {
initialLocation = coordinate
} else {
print("Error at forwardGeocoding #willcohen #ERRORALERT")
}
dispatch_group_leave(dispatchGroup)
})
self.forwardGeocodingDesired("\(addressTxtFldDest.text) \(cityTxtFldDest.text), \(stateTxtFldDest.text)", completion: {
success, coordinate in
if success {
desiredLocation = coordinate
} else {
print("Error at forwardGeocodingDesired #willcohen #ERRORALERT")
}
dispatch_group_leave(dispatchGroup)
})
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue()) {
completion(initialLocation:initialLocation,desiredLocation:desiredLocation)
}
}
Usage:
getLatsAndLongs { (initialLocation, desiredLocation) in
print(initialLocation)
print(desiredLocation)
}
Check if the completion blocks in self.forwardGeocoding and self.forwardGeocodingDesired execute asynchronously. If so, the values will not be set until after the prints have run.
I would like to have multiple markers and update their position, based on new data from server. This is how I show marker on map (basic stuff):
func showMarkerOnMap() {
mapView.clear() //<- That's GMSMapView
for all in dataFromServer {
let lat5 = all.latitude
let lon5 = all.longitude
let position = CLLocationCoordinate2DMake(lat5, lon5)
let marker = GMSMarker(position: position)
marker.flat = true
marker.map = self.mapView
}
}
This is how I get dataFromServer using Alamofire:
var dataFromServer = [dataClass]()
func getCoordinatesFromServer(){
//Here goes headers and authentication data
Alamofire.request(.GET, URL, headers: headers).authenticate(user: oranges, password: appels).responseJSON { response in
switch response.result {
case .Success:
//Remove existing dataFromServer
self.dataFromServer.removeAll()
if let value = response.result.value {
let json = JSON(value)
for result in json.arrayValue {
let lat = result["latitude"].doubleValue
let lon = result["longitude"].doubleValue
let zip = dataClass(latitude: lat, longitude: lon)
//The next part is for checking that coordinates do not overlap
if self.dataFromServer.count < 1 {
self.dataFromServer.append(zip)
} else {
for all in self.dataFromServer {
guard all.latitude != lat else {
return
}
self.trblInfo1.append(zip)
}
}
}
//This should update existing markers?
self.showMarkerOnMap()
}
case .Failure(let error):
print(error)
}
}
}
Basically I just append all received data to my dataFromServer which belongs to dataClass class:
class dataClass: NSObject {
var latitude: Double
var longitude: Double
init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
}
My getCoordinatesFromServer() function is being called every 3 seconds (for now). What I was expecting to receive new coordinates (and I do receive them for sure), thenshowMarkerOnMap() should be called thus clearing all existing markers and creating news. What I get - marker duplicate and noticeable lag. The original marker disappears if go to another View and then comeback to View containing mapView.
Any suggestion on how to improve my code or some alternative?
If you have any kind of unique identifier for your positions that came from server, you can keep a list of markers and then update their location when new data arrive. Something like this:
for result in json.arrayValue {
let lat = result["latitude"].doubleValue
let lon = result["longitude"].doubleValue
let identifier = result["identifier"] as! String
self.myMarkersDictionary[identifier]?.position.latitude = lat
self.myMarkersDictionary[identifier]?.position.longitude = lon
...
}