CLGeoCoder doesn't append placemarks in right order - ios

In my viewDidLoad I have this method which turns a location from Parse into a city and state. The only problem is, is that it appends in the wrong order most of the time. Also the .location I have on the end of the location variable is a custom method that turns a PFGeoPoint into a CLLocation.
for post in posts {
let location = ((post["locations"] as! NSMutableArray)[0] as! PFGeoPoint).location
CLGeocoder().reverseGeocodeLocation(location()) { (placemarks, error) -> Void in
if error != nil {
print(error)
} else {
let p = CLPlacemark(placemark: placemarks![0])
let locationString = "\(p.locality!), \(p.administrativeArea!)"
self.locationStrings.append(locationString)
}
}
}
I believe this is all happening due to the CLGeoCoder completion handler takes a while to fully execute. I think this could be solved by doing all of this without the completion hander, but I'm not sure how to do that. Thank you!

Reverse geocoding needs to be done asynchronously to prevent lag. You can create an object to hold your post location data, or your post data in general, with a location string property. Instantiate the post object and add them to an array then iterate over them to retrieve the location string and set it on the object. For example:
for postObject in postObjects {
CLGeocoder().reverseGeocodeLocation(postObject.location) { (placemarks, error) -> Void in
if error != nil {
print(error)
} else {
let p = CLPlacemark(placemark: placemarks![0])
postObject.locationString = "\(p.locality!), \(p.administrativeArea!)"
}
}
}

Related

How to get progress of asynchronous Firebase data read?

I have some code that reads data from Firebase on a custom loading screen that I only want to segue once all of the data in the collection has been read (I know beforehand that there won't be more than 10 or 15 data entries to read, and I'm checking to make sure the user has an internet connection). I have a loading animation I'd like to implement that is started by calling activityIndicatorView.startAnimating() and stopped by calling activityIndicatorView.stopAnimating(). I'm not sure where to place these or the perform segue function in relation to the data retrieval function. Any help is appreciated!
let db = Firestore.firestore()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else{
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
}
You don't need to know the progress of the read as such, just when it starts and when it is complete, so that you can start and stop your activity view.
The read starts when you call getDocuments.
The read is complete after the for loop in the getDocuments completion closure.
So:
let db = Firestore.firestore()
activityIndicatorView.startAnimating()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else {
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
DispatchQueue.main.async {
activityIndicatorView.stopAnimating()
}
}
As a matter of style, having multiple arrays with associate data is a bit of a code smell. Rather you should create a struct with the relevant properties and create a single array of instances of this struct.
You should also avoid force unwrapping.
struct PackageInfo {
let id: String
let name: String
let imageId: String
let radius: String
}
...
var packages:[PackageInfo] = []
...
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else if let documents = snapshot?.documents {
self.packages = documents.compactMap { doc in
if let title = doc.get("title") as? String,
let imageId = doc.get("imgID") as? String,
let radius = doc.get("radius") as? String {
return PackageInfo(id: doc.documentID, name: title, imageId: imageId, radius: radius)
} else {
return nil
}
}
}
There is no progress reporting within a single read operation, either it's pending or it's completed.
If you want more granular reporting, you can implement pagination yourself so that you know how many items you've already read. If you want to show progress against the total, this means you will also need to track the total count yourself though.

Function runs after block of code even though it is called before

Ok I don't get this. I have written some code for forward geocoding, I have an UITextField that you write name of a city in and after you press the enter button it is dismissed and at the same time the function is called to determine if the UITextField contains a valid input. If there is an error it is saved in a bool variable which value is changed in the function. I have print statements all over the place and from the console output I can see that the function ran after the if condition, but it is called before... what? Can somebody explain me what is going on? Code:
var locationError: Bool?
func textFieldShouldReturn(textField: UITextField) -> Bool {
self.view.endEditing(true)
forwardGeocoding(textField.text!)
print("forward geocoding ran 1st time")
print(locationError)
if locationError == true {
print("Error")
} else if locationError == false {
print("Success")
} else if locationError == nil {
print("No value for locationError")
}
return false
}
func forwardGeocoding(address: String) -> CLLocation? {
var userLocation: CLLocation?
CLGeocoder().geocodeAddressString(address, completionHandler: { (placemarks, error) in
if error != nil {
print("Geocoding error: \(error)")
self.locationError = true
return
}
if placemarks?.count > 0 {
print("Placemark found")
self.locationError = false
let placemark = placemarks?.first
let location = placemark?.location
let coordinate = location?.coordinate
print("Settings location: \(coordinate!.latitude), \(coordinate!.longitude)")
if let unwrappedCoordinate = coordinate {
let CLReadyLocation: CLLocation = CLLocation(latitude: unwrappedCoordinate.latitude, longitude: unwrappedCoordinate.longitude)
userLocation = CLReadyLocation
}
}
})
return userLocation
}
Console output:
forward geocoding ran 1st time
nil
No value for locationError
Placemark found
Settings location: 48.8567879, 2.3510768
Try multi threading it...
let queue = NSOperationQueue()
queue.addOperationWithBlock() {
NSOperationQueue.mainQueue().addOperationWithBlock() {
}
}
You need to add the completion handler as your function parameter:
func forwardGeocoding(address: String, completionHandler: (placemarks: String? or [Array of any type], error: NSError?) -> ()) -> CLLocation?
modify your if block:
if error != nil {
print("Geocoding error: \(error)")
self.locationError = true
completionHandler(nil, error)
return
}
then call it like
forwardGeocoding(textField.text!){(placemarks, error) in
//your code.
}
When you call geocodeAddressString, it is executed in a separate thread (#AppleDoc This method submits the specified location data to the geocoding server asynchronously and returns). So effectively you have 2 threads running in parallel. geocodeAddressString in thread 2 will take more time to execute since it makes a server call and the block will be executed when the call returns. During this time thread 1 will finish its execution and will print the log statements.
If you want to handle this issue, locationError if-else condition logic should be implemented in such a way that it should be triggered once your callback is executed.

Initializer For Conditional Binding Must Have Optional Type, not CLPlacemark

Noob programmer here.
I'm trying to become a good programmer with the help of the internet. So, I subscribed to Rob Percivals iOS class. I'm having trouble with this bit of my code. Must've been a recent update that's making it fail on my system although it is the same code as his.
CLGeocoder().reverseGeocodeLocation(userLocation) { (placemarks, error) in
if (error != nil) {
print(error)
} else {
if let p = CLPlacemark(placemark: placemarks![0] as! CLPlacemark) {
print(p)
}
}
}
}
Please try to put it in baby english if possible guys. Thanks!
placemarks is of type [CLPlacemark]?. Therefore you should try something like the following:
if let placemarks = placemarks {
if let p0 = placemarks.first {
print (p0)
}
}
Don't use !. Use if let instead.
The error message just says that the initializer CLPlacemark(placemark:) does never return a nil value so reduce the code and even remove the type casting, because according to the signature [CLPlacemark]?is returned. Optional binding is not needed because if the error is nil the array is guaranteed to exist.
...
} else {
if !placemarks!.isEmpty {
let p = CLPlacemark(placemark: placemarks![0])
print(p)
}
}
I'd recommend taking it a step further with a guard statement, and being a little more verbose with the variable name p... Guard is a good way to exit from the current flow without cluttering your code with if statements, especially since the 'if let' optional unwrapping syntax came around.
CLGeocoder().reverseGeocodeLocation(userLocation) { (placemarks, error) in
guard error == nil else {
print(error)
return
}
if let placemark = placemarks?.first {
let currentPlacemark = CLPlacemark(placemark: placemark)
print(currentPlacemark)
}
}

CLGeocoder error EXC_BAD_INSTRUCTION

I'm using CLGeocoder reverseGeocodeLocation. I get the crash after running for about 5-10 minutes (with no noticeable pattern) and get random crashes. Here's my code:
if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {
let currentLatCoord = manager.location?.coordinate.latitude
let currentLongCoord = manager.location?.coordinate.longitude
CLGeocoder().reverseGeocodeLocation(CLLocation(latitude: currentLatCoord!, longitude: currentLongCoord!)) { (placemarks, error) -> Void in
if error != nil {
print(error)
return
}
let placeArray = placemarks as [CLPlacemark]!
var placeMark: CLPlacemark
placeMark = placeArray![0]
self.locationLabel.text = String(placeMark.addressDictionary?["Thoroughfare"]!)
}
}
And also, just to help, here's a picture of the line and error:
I think you need some optional binding:
if let thoroughfare = placeMark.addressDictionary?["Thoroughfare"] as? String {
self.locationLabel.text = thoroughfare
}
I'm guessing either there might not be a "Thoroughfare" key in the address dictionary, and you're providing a nil value to the designated initializer for String.
Is there a chance that the view being updated in your code snippet is not on the screen (disposed) when the CLGeocoder has finished its reverse geocoding? If you have your outlet defined as an implicitly unwrapped optional:
#IBOutlet var locationLabel : UILabel!
I'm wondering if it has already been set to nil, but due to the bang (!) the compiler isn't making you check.
But, of course, if your view is still on the screen when you crash, this probably isn't the issue.
You provided us a code sample:
let currentLatCoord = manager.location?.coordinate.latitude
let currentLongCoord = manager.location?.coordinate.longitude
CLGeocoder().reverseGeocodeLocation(CLLocation(latitude: currentLatCoord!, longitude: currentLongCoord!)) { (placemarks, error) -> Void in
if error != nil {
print(error)
return
}
let placeArray = placemarks as [CLPlacemark]!
var placeMark: CLPlacemark
placeMark = placeArray![0]
self.locationLabel.text = String(placeMark.addressDictionary?["Thoroughfare"]!)
}
You can more gracefully handle nil values if you use the if let construct:
CLGeocoder().reverseGeocodeLocation(manager.location!) { placemarks, error in
guard error == nil else {
print(error)
return
}
if let placemark = placemarks?.first {
self.locationLabel.text = placemark.thoroughfare
}
}
And, of course, if you're calling this repeatedly, I wouldn't re-instantiate a new CLGeocoder every time, but hopefully this illustrates the pattern.
But as you can see, you can avoid extracting the latitude and longitude from the location property to only then create a new CLLocation object by simply using manager.location directly. Likewise, you can use the thoroughfare property, which saves you from needing to cast the addressDictionary value.
The key observation, which Craig mentioned above, is to scrupulously avoid using the ! forced unwrapping operator unless you are positive that the variable can never be nil. Likewise, don't use [0] syntax unless you know for a fact that there is at least one item in the array (which is why I use first, which is an optional for which I can easily test).
Frankly, I'd even make sure that the location was valid (not nil and with a non-negative horizontalAccuracy, as a negative value indicates that the coordinates are not valid):
if let location = manager.location where location.horizontalAccuracy >= 0 {
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
guard error == nil else {
print(error)
return
}
if let placemark = placemarks?.first {
self.locationLabel.text = placemark.thoroughfare
}
}
}

How can I access a variable outside block? [duplicate]

This question already has an answer here:
AlamoFire GET api request not working as expected
(1 answer)
Closed 8 years ago.
Following code doesn't work properly.
func convertToStreet(location:CLLocationCoordinate2D) -> CLPlacemark {
var tempLocation = CLLocation(latitude: location.latitude, longitude: location.longitude)
var temPlacemark:CLPlacemark?
CLGeocoder().reverseGeocodeLocation(tempLocation, completionHandler: {(placemarks, error) in
temPlacemark = (placemarks[0] as CLPlacemark)
println(temPlacemark!.thoroughfare)
})
return temPlacemark!
}
Println inside the completion handler works properly, but the value of temPlacemark is nil at the end of code. Why does that happen? I thank you very much in advance.
It's because that completionHandler is called asynchronously. For that to work you should have a callback block in your custom function to return the value once you get it from the CLGeocoder.
Something like this:
func convertToStreet(coordinate: CLLocationCoordinate2D, completionHandler: (placemark: CLPlacemark!, error: NSError!) -> Void) {
let tempLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
CLGeocoder().reverseGeocodeLocation(tempLocation) { placemarks, error in
completionHandler(placemark: placemarks?.first as CLPlacemark?, error: error)
}
}
You'd then call it like so:
convertToStreet(location.coordinate) { placemark, error in
if placemark != nil {
// use `placemark` here
println(placemark.thoroughfare)
} else {
println(error)
}
}
// but don't use `placemark` here
The problem of your code is that the block code :
temPlacemark = (placemarks[0] as CLPlacemark)
println(temPlacemark!.thoroughfare)
will be executed later.
Meaning that the current return statement will always return an uninitialized value.
If you want to initialize this var in your block you should make it a property of your object.

Resources