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

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.

Related

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.

How to get GMSPlace for a CLLocationCoordinate2D?

I have a location coordinate in the form of a CLLocationCoordinate2D. How do I get its equivalent GMSPlace object using the Google Maps SDK?
This seems like it should be a very simple task, but I couldn't find anything in Google's documentation or on Stack Overflow.
I am working on a similar issue, and I haven't found the exact solution but these alternatives may work depending on your situation. If you are okay with having a GMSAddress instead of a GMSPlace, you may use a GMSGeocoder with a call to reverseGeocodeCoordinate as seen in option two below.
Two options if you're trying to get the user's current location:
Use Google Maps current location to get a GMSPlace. This is pretty simple and solves your problem if you are okay with only resorting to actual places. The problem with this is that I couldn't figure out how to get all addresses (as opposed to businesses). You can see the documentation here.
In viewDidLoad:
let placesClient = GMSPlacesClient()
When you want to get the current place:
placesClient?.currentPlaceWithCallback({ (placeLikelihoods, error) -> Void in
if error != nil {
// Handle error in some way.
}
if let placeLikelihood = placeLikelihoods?.likelihoods.first {
let place = placeLikelihood.place
// Do what you want with the returned GMSPlace.
}
})
Use OneShotLocationManager to get the CLLocationCoordinate2D and turn it into a GMSAddress. You will have to replace the _didComplete function with the code below to return a GMSAddress instead of a CLLocationCoordinate2D.
private func _didComplete(location: CLLocation?, error: NSError?) {
locationManager?.stopUpdatingLocation()
if let location = location {
GMSGeocoder().reverseGeocodeCoordinate(location.coordinate, completionHandler: {
[unowned self] (response, error) -> Void in
if error != nil || response == nil || response!.firstResult() == nil {
self.didComplete?(location: nil,
error: NSError(domain: self.classForCoder.description(),
code: LocationManagerErrors.InvalidLocation.rawValue,
userInfo: nil))
} else {
self.didComplete?(location: response!.firstResult(), error: error)
}
})
} else {
self.didComplete?(location: nil, error: error)
}
locationManager?.delegate = nil
locationManager = nil
}
Someone posted on here a convenient wrapper to extract fields from the GMSAddressComponents that you may find useful when dealing with this API. This makes it easy because then when you want to access the city all you have to do is place.addressComponents?.city as an example.
extension CollectionType where Generator.Element == GMSAddressComponent {
var streetAddress: String? {
return "\(valueForKey("street_number")) \(valueForKey(kGMSPlaceTypeRoute))"
}
var city: String? {
return valueForKey(kGMSPlaceTypeLocality)
}
var state: String? {
return valueForKey(kGMSPlaceTypeAdministrativeAreaLevel1)
}
var zipCode: String? {
return valueForKey(kGMSPlaceTypePostalCode)
}
var country: String? {
return valueForKey(kGMSPlaceTypeCountry)
}
func valueForKey(key: String) -> String? {
return filter { $0.type == key }.first?.name
}
}

CLGeoCoder doesn't append placemarks in right order

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!)"
}
}
}

how to return value after the execution of the block? Swift

I want to get a value from function. There is a block in function. When block executes the function already returns the value. I tried many different methods but they didn't help me. I used NSOperation and Dispatch. The function always returns value until execution of block.
var superPlace: MKPlacemark!
func returnPlaceMarkInfo(coordinate: CLLocationCoordinate2D) -> MKPlacemark? {
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
geocoder.reverseGeocodeLocation(location) { (arrayPlaceMark, error) -> Void in
if error == nil {
let firstPlace = arrayPlaceMark!.first!
let addressDictionaryPass = firstPlace.addressDictionary as! [String : AnyObject]
self.superPlace = MKPlacemark(coordinate: location.coordinate, addressDictionary: addressDictionaryPass)
}
}
return superPlace
}
You cannot simply return here as the reverseGeocodeLocation function is running asynchronously so you will need to use your own completion block:
var superPlace: MKPlacemark!
func getPlaceMarkInfo(coordinate: CLLocationCoordinate2D, completion: (superPlace: MKPlacemark?) -> ()) {
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
geocoder.reverseGeocodeLocation(location) { (arrayPlaceMark, error) -> Void in
if error == nil {
let firstPlace = arrayPlaceMark!.first!
let addressDictionaryPass = firstPlace.addressDictionary as! [String : AnyObject]
self.superPlace = MKPlacemark(coordinate: location.coordinate, addressDictionary: addressDictionaryPass)
completion(superPlace: superPlace)
} else {
completion(superPlace: nil)
}
}
}
This comes up over and over and over. The short answer is "you can't."
The result is not available when your function returns. The async call takes place in the background.
What you want to do is refactor your returnPlacemarkInfo function to take a completion closure.
I have been working in Objective-C lately, so my Swift is a little rusty, but it might look like this:
func fetchPlaceMarkInfo(
coordinate: CLLocationCoordinate2D,
completion: (thePlacemark: MKPlacemark?) -> () )
{
}
Then when you call it, pass in a completion closure that gets invoked once the placemark is available.
EDIT:
I wrote a demo project and posted it on Github that simulates handling an async network download. Take a look at
https://github.com/DuncanMC/SwiftCompletionHandlers
Specifically look at the method asyncFetchImage(), which does almost exactly what we are talking about: Uses an async method internally, and takes a completion block that it calls once the async load is done.

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
}
}
}

Resources