reverseGeocode - completionHandling a completionHandler? (Swift) - swift

What is the best way to achieve this? I have a func that gets the reverseGeocode for a given longitude/latitude and it works just fine. But because it is asynchronous by the time it has got the address information I have already executed the lines to commit the address to a database. Do I need to completionHandle the completionHandler or is there some other way. I did try a do/while loop (don't laugh) and that didn't work.
func reverseGeocode() -> Bool {
let geoCoder = CLGeocoder()
let newLocation = CLLocation(latitude: valueLatitude, longitude: valueLongitude)
geoCoder.reverseGeocodeLocation(newLocation, completionHandler: {(placemarks: [AnyObject]!, error: NSError!) in
if error != nil {
println("LocationsMenu - Geocode failed with error: \(error.localizedDescription)")
}
if placemarks.count > 0 {
let myplacemark = placemarks[0] as! CLPlacemark
let addressDictionary = myplacemark.addressDictionary
let address = addressDictionary[kABPersonAddressStreetKey] as! NSString
let city = addressDictionary[kABPersonAddressCityKey] as! NSString
let state = addressDictionary[kABPersonAddressStateKey] as! NSString
let postcode = addressDictionary[kABPersonAddressZIPKey] as! NSString
let country = addressDictionary[kABPersonAddressCountryKey] as! NSString
println("\(address) \(city) \(state) \(postcode) \(country)")
}
self.showMap(placemarks[0] as! CLPlacemark)
})
}

You simply need to move the code that commits your placemark information to the database inside your completion handler. If that code is called outside your reverseGeocode method, you can refactor reverseGeocode to take a block as a parameter. Then invoke that block inside your completion block (presumably inside your if placemarks.count > 0 if block.)

Related

Swift cannot use found values out of reverseGeocodeLocation()

I tried to store the city found by the reverseGeocodeLocation method inside a variable but it seems that it's not possible with the following code to reuse it inside a variable decelerated before. Debugger showed me that a city is found and is stored inside returnCity but only during execution of the reverseGeocodeLocation method.
I found some other posts but none of them helped me with my problem.
Thank you for any help!
// Get City out of coordinates
var returnCity = ""
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: self.selectedLat, longitude: self.selectedLng)
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
// Place details
var placeMark: CLPlacemark!
placeMark = placemarks?[0]
// City
if let foundCity = placeMark.locality {
returnCity = foundCity
}
})
NSLog("Sub Locality is: " + returnCity) // console shows "Sub Locality is: "
You need to use property observer to update your UI or use the updated result. I would modify "returnCity" property like below and use the data as soon as it is set and every time it is changed inside a didSet observer.
var returnCity:String? {
didSet {
guard let returnCity = self.returnCity else {return}
NSLog("Sub Locality is: " + returnCity)
}
}

How To Call a func within a Closure

In a model's class Location, I get the name of the current city:
var currentLatitude: Double!
var currentLongitude: Double!
var currentLocation: String!
var currentCity: String!
func getLocationName() {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
self.nowUpdateUI()
})
}
In view controller I want to update the UI and update my label to show the current city.
However, self.currentCity = city happens inside of a closure. So if I just run a func in view controller:
func updateUI() {
cityLbl.text = Location.sharedInstance.currentCity
}
I'm not getting anywhere because the closure haven't finished running.
I've been advised to add a completion handler to getLocationName() and inside of it, perform the call to a func that will update the UI.
However, from all the tutorials out there on closures, completion handlers, it is not clear to me how to achieve that.
How to construct a completion handler, pass it as an arg to getLocationName() and how to call getLocationName from view controller?
To handle this situation you have multiple option.
Create delegate/protocol with your Location class
Create one protocol and implement that protocol method with your ViewController and declare its instance in your Location class. After then in the completionHandler of reverseGeocodeLocation call this delegate method. Check Apple documentation on Protocol for more details.
You can create completionHandler with your getLocationName method of Location class.
Add completionHandler with getLocationName and called that completionHandler inside the completionHandler of reverseGeocodeLocation like this way.
func getLocationName(completionHandler: #escaping (_ success: Bool) -> Void) {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
completionHandler(false)
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
completionHandler(true)
//self.nowUpdateUI()
})
}
Now in ViewController where you are calling this function call your updateUI method inside the completion block.
Location.sharedInstance.getLocationName { (success) in
if success {//If successfully got response
self.updateUI()
}
}
You can add observer for (NS)NotificationCenter.
Register the observer with (NS)NotificationCenter and then post the notification inside the completionHandler of reverseGeocodeLocation. You can get more detail on this with this StackOverflow Post.
// I thing issue back ground thread you need to update your UI in main thread
var currentLatitude: Double!
var currentLongitude: Double!
var currentLocation: String!
var currentCity: String!
func getLocationName() {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
DispatchQueue.main.async {
self.nowUpdateUI()
// Update your UI in main thread
}
})
}
This entire piece of your code:
completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
self.nowUpdateUI()
}
)
is already happening in the completionHandler (which happens after everything is finished) Just also run your updateUI() inside the completionHandler. So your end code would be :
completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
DispatchQueue.main.async {
updateUI()
}
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
self.nowUpdateUI()
}
)
The reason you have to use DispatchQueue.main is because your completionHandler is on a backgroundqueue but you MUST always do you UI related stuff from your mainQueue—so users get the fastest changing in their UI without any glitches. Imagine if you were doing on a background thread and it was happening slow

EXC Bad Instruction

In was wondering why I keep getting this error message, EXC Bad Instruction could someone help me out and tell me why.
Here is the code.
func updateStocks() {
let stockManager:StockManagerSingleton = StockManagerSingleton.sharedInstance
stockManager.updateListOfSymbols(stocks)
//Repeat this method after 15 secs. (For simplicity of the tutorial we are not cancelling it never)
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(15 * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(),
{
self.updateStocks()
}
)
}
//4
func stocksUpdated(notification: NSNotification) {
let values = (notification.userInfo as! Dictionary<String,NSArray>)
let stocksReceived:NSArray = values[kNotificationStocksUpdated]!
stocks.removeAll(keepCapacity: false)
for quote in stocksReceived {
let quoteDict:NSDictionary = quote as! NSDictionary
let changeInPercentString = quoteDict["ChangeInPercent"] as! String
let changeInPercentStringClean: NSString = (changeInPercentString as NSString).substringToIndex((changeInPercentString as NSString).length-1)
stocks.append(quoteDict["symbol"] as! String,changeInPercentStringClean.doubleValue)
}
tableView.reloadData()
NSLog("Symbols Values updated :)")
}
}
The line with the error in it is,
let changeInPercentString = quoteDict["ChangeInPercent"] as! String
The error states that Swift attempted to unwrap a nil value, as you stated on this line
let changeInPercentString = quoteDict["ChangeInPercent"] as! String
Swift attempts to force setting the value of quoteDict["ChangeInPercent"] to a String, because you use as!, instead, you should use as?, which will set the value to nil if the value cannot be found
let changeInPercentString = quoteDict["ChangeInPercent"] as? String
You could set this to a default value by using the ?? operator. For example, if you wanted the default value to be 0.0%, you could use
let changeInPercentString = (quoteDict["ChangeInPercent"] as? String) ?? "0.0%"
The inherent problem is most likely either that quoteDict["ChangeInPercent"] does not exist, or quoteDict["ChangeInPercent"] is not a String - it may be an NSString or simply a Double value.
If you find out that it is supposed to be an NSString, for example, you will need to change how you cast the value
let changeInPercentString: NSString = (quoteDict["ChangeInPercent"] as? NSString) ?? "0.0%"

Swift Completion Handler for Reverse Geocoding

I have been banging my head in order to figure out how to fix this piece of code. Basically I have a piece of code that takes a string cityName and stores the latitude and longitude of it in a global variable and call it in another function right after. Apparently because of asynchronous call, I am not able to do that and the value of longitude and latitude are nil.
func findCityCoordinates(cityName: String) {
var geocoder = CLGeocoder()
geocoder.geocodeAddressString(cityName, completionHandler: {(placemarks: [AnyObject]!, error: NSError!) -> Void in
if let placemark = placemarks?[0] as? CLPlacemark {
self.cityLatitude = placemark.location.coordinate.latitude //Returns nil
self.cityLongitude = placemark.location.coordinate.longitude //Returns nil
}
})
}
I have also been trying to work around completion handler but I have no idea on how to implement it and call it in a function. I would appreciate some help.
I was able to use the dispatch async for this. I declared two variables above the geocoder, assign them inside, and use them after it completes.
var lat:Float!
var long:Float!
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(cityName, completionHandler: {(placemarks: [AnyObject]!, error: NSError!) -> Void in
if let placemark = placemarks?[0] as? CLPlacemark {
lat = Float(placemark.location.coordinate.latitude)
long = Float(placemark.location.coordinate.longitude)
}
dispatch_async(
dispatch_get_main_queue(), {
self.cityLatitude = lat
self.cityLongitude = long
})
})
...stores the latitude and longitude of it in a global variable and call it in another function right after
I suspect that the other function is using it before the geocoder completion block sets the values. Put the call to the other function to the completion block, if possible.
func findCityCoordinates(cityName: String) {
var geocoder = CLGeocoder()
geocoder.geocodeAddressString(cityName, completionHandler: {(placemarks: [AnyObject]!, error: NSError!) -> Void in
if let placemark = placemarks?[0] as? CLPlacemark {
self.cityLatitude = placemark.location.coordinate.latitude //Returns nil
self.cityLongitude = placemark.location.coordinate.longitude //Returns nil
*** <-- call your function that uses location here --> ***
}
})
}
Well my brother helped me a little bit out on this, basically I wanted to run a completion block in the IBAction of the save button instead inside of the function findCityCoordinates:
func findCityCoordinate(city: String, completionHandler: (coordinate: CLLocationCoordinate2D) -> ()) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(city) { (placemarks: [AnyObject]!, error: NSError!) -> () in
       if let placemark = placemarks[0] as? CLPlacemark {
           let coordinate = placemark.location.coordinate
          completionHandler(coordinate: coordinate)
         }
     }
}
And heres the function being called inside the saveButton action outlet:
findCityCoordinates(searchBar.text) { (cityCoordinate: CLLocationCoordinate2D) -> () in
self.queryForTable(cityCoordinate)
// Force reload of table data
self.myTableView.reloadData()
}

Swift Reverse Geocoding using the Data

I am successfully getting the current address details based on my location. It printlns perfectly. What is throwing me is how I extract the data from this call. I have tried passing, say the ZIP/Postcode, as local and even global variables but with no joy. The data only seems to exist within this call. How can I use it elsewhere?
// Get Address Information
let geoCoder = CLGeocoder()
let newLocation = CLLocation(latitude: valueLatitude, longitude: valueLongitude)
geoCoder.reverseGeocodeLocation(newLocation, completionHandler: {(placemarks: [AnyObject]!, error: NSError!) in
if error != nil {
println("Geocode failed with error: \(error.localizedDescription)")
}
if placemarks.count > 0 {
let placemark = placemarks[0] as! CLPlacemark
let addressDictionary = placemark.addressDictionary
let address = addressDictionary[kABPersonAddressStreetKey] as! NSString
let city = addressDictionary[kABPersonAddressCityKey] as! NSString
let state = addressDictionary[kABPersonAddressStateKey] as! NSString
let postcode = addressDictionary[kABPersonAddressZIPKey] as! NSString
let country = addressDictionary[kABPersonAddressCountryKey] as! NSString
println("\(address) \(city) \(state) \(postcode) \(country)") }
})
Your problem is most likely due to the fact that reverseGeocodeLocation is an asynchronous request made to Apple servers.
What needs to happen is:
You call reverseGeocodeLocation
reverseGeocodeLocation finishes, starts its completion which calls a method passing the placemark you just recovered.
In order to do that:
#IBAction func btnInsertClicked(sender: AnyObject) {
var locationRecord: LocationRecord = LocationRecord()
// Get Address Information
let geoCoder = CLGeocoder()
let newLocation = CLLocation(latitude: valueLatitude, longitude: valueLongitude)
geoCoder.reverseGeocodeLocation(newLocation, completionHandler:
{(placemarks: [AnyObject]!, error: NSError!) in
if error != nil {
println("Geocode failed with error: (error.localizedDescription)")
}
if placemarks.count > 0 {
let myPlacemark = placemarks[0] as! CLPlacemark
// Here call the method that uses myPlacemark
self.myAwesomeMethod(placemarks[0] as! CLPlacemark)
} else {
println("No placemark")
}
})
}
Where you need to use it in your code:
func myAwesomeMethod(placemark: CLPlacemark) {
// Do stuff with placemark
}
I won't have my mac until tonight but if that doesn't work, leave a comment and we will work this out