How to return a value from CLGeocoder? - ios

I'm working on the weather app as a training and there's a need to convert location to city. So I'm using CLGeocoder like so:
func updateWeatherData(json: JSON?) {
if let json = json {
weatherData.temperature = fahrenheitToCelcius(json["currently"]["temperature"].doubleValue)
weatherData.weatherIconName = json["currently"]["icon"].stringValue
let location = CLLocation(latitude: json["latitude"].doubleValue, longitude: json["longitude"].doubleValue)
CLGeocoder().reverseGeocodeLocation(location) { (placemark, error) in
if let city = placemark {
self.weatherData.city = city.last?.locality
} else if let error = error {
print(error)
}
}
updateUIWithWeatherData()
}
}
//MARK: - UI Updates
func updateUIWithWeatherData() {
cityLabel.text = weatherData.city
temperatureLabel.text = "\(weatherData.temperature)°"
weatherIcon.image = UIImage(named: weatherData.weatherIconName!)
}
And this code returns nil to weatherData.city. But when I place a breakpoint inside a closure, everything works fine. What am I missing?

If I understand your issue correctly you need to update your UI after the geocoding is complete. Like the following:
func updateWeatherData(json: JSON?) {
if let json = json {
weatherData.temperature = fahrenheitToCelcius(json["currently"]["temperature"].doubleValue)
weatherData.weatherIconName = json["currently"]["icon"].stringValue
let location = CLLocation(latitude: json["latitude"].doubleValue, longitude: json["longitude"].doubleValue)
CLGeocoder().reverseGeocodeLocation(location) { (placemark, error) in
if let city = placemark {
self.weatherData.city = city.last?.locality
} else if let error = error {
print(error)
}
self.updateUIWithWeatherData()
}
}
}
The order of these operations is so that the geocoding is done asynchronously and may occur later then the code called after it. Note may but does not need to.
You should also read documentation of this method about threading. UI must be updated on main thread so unless the documentation specifies that the call will be done on main thread you are best forcing it:
CLGeocoder().reverseGeocodeLocation(location) { (placemark, error) in
if let city = placemark {
self.weatherData.city = city.last?.locality
} else if let error = error {
print(error)
}
DispatchQueue.main.async {
self.updateUIWithWeatherData()
}
}

Related

forward geocoding completionHandler is not executing in iOS swift4

I am trying convert to addresses into latlongs to place them on googleMap but, when I pass the address String to geoCoder.geoAddressString(fullAddress) and running application, completionHandler is not executing, I tried with breakpoint at geoCoder.geocodeAddressString(fullAddress) { (placemarks, error) and when I click on stepover it is coming out if the completionHandler..
Now, I have no idea why this is happening... Does anyone know? please Help..
see My code Here:
func loadMarkers() {
for i in 0..<self.professioanlDetailsAH.count {
let fullAddress = "\(self.professioanlDetailsAH[i]["searchAddress"]!)"
if !fullAddress.isEmpty {
var geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(fullAddress) { (placemarks, error) in
guard
let placemarks = placemarks,
let addressLocation = placemarks.first?.location
else {
// handle no location found
return
}
let latitude = addressLocation.coordinate.latitude
let longitude = addressLocation.coordinate.longitude
}
}
}
}
Edit:
func loadMarkers() {
for i in 0..<self.professioanlDetailsAH.count {
print(self.professioanlDetailsAH[i]["firstName"]!)
let fullAddress = "\(self.professioanlDetailsAH[i]["searchAddress"]!)"
print(fullAddress)
if !fullAddress.isEmpty {
geoCoder.geocodeAddressString(fullAddress) {(placemarks, error) in
if error != nil {
print(error!)
}
else{
guard
let placemarks = placemarks,
let addressLocation = placemarks.first?.location
else {return}
let latitude = addressLocation.coordinate.latitude
let longitude = addressLocation.coordinate.longitude
print("lat:\(latitude), long:\(longitude)")
}
}
}
}

Swift: Asynchronous call in a for loop

I am trying to do the following in swift - Trying to reverse decode a list of addresses in an array and print their latitude/longitude coordinates. The code I have is as follows.
let addressArray = ["Address 1", "Address 2"]
var coordinatesArray = [CLLocationCoordinate2D]()
override func viewDidLoad() {
super.viewDidLoad()
createAddressList()
printAddressList()
}
func printAddressList() {
for i in 0 ..< addressArray.count {
print("Address = \(addressArray[i]) Coordinates = \(coordinatesArray[i].latitude),\(coordinatesArray[i].latitude)")
}
func createAddressList() {
for i in 0 ..< addressArray.count {
let address = addressArray[i]
geocoder.geocodeAddressString(address, completionHandler: {(placemarks, error) -> Void in
print("Address = \(address)");
if let placemark = placemarks?.first {
let coordinate = placemark.location?.coordinate
self.coordinatesArray.append(coordinate!)
}
})
}
}
}
The code prints only the first address that's decoded and nothing happens then.
I do have a fix for this like the below one, which is to move the printAddressList call from viewDidLoad method like this
func createAddressList() {
if count < self.addressArray.count {
let address = addressArray[count]
geocoder.geocodeAddressString(address, completionHandler: {(placemarks, error) -> Void in
print("Address = \(address)");
if let placemark = placemarks?.first {
let coordinate = placemark.location?.coordinate
self.coordinatesArray.append(coordinate!)
}
print("Count = \(self.count)")
self.count += 1
self.createAddressList()
})
} else {
printAddressList()
}
}
Even though the latter solution works, I see that it's not clean, would like to know the right way to do this while making the code readable and clean.
How about using this structure?
let workGroup = dispatch_group_create()
for i in 0..<addressArray.count {
dispatch_group_enter(workGroup)
performGeoCoding({ successCallback :
dispatch_group_leave(workGroup)
})
}
dispatch_group_notify(workGroup, dispatch_get_main_queue()){
successCallback()
printAddressList()
}
There is very nice tutorial about dispatch_group here.
A bit more updated would be something like:
let dispatchGroup = DispatchGroup()
for address in addressArray {
dispatchGroup.enter()
performGeoCoding { address in
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
completionHandler()
}

GCD Semaphore does not wait (Swift)

I'm pretty much new to GCD. I have this function for forward geocoding and the issue is it returns before the completion closure completes. So every time it just returns nil. I found out I can use semaphores so the return waits for the completion closure to complete, but there are very little examples online and I found none of a function that returns. I tried to implement it, but function still returns nil even though the location is printed out to the console moments later. If someone could tell me where I am making a mistake I would be very grateful.
func forwardGeocoding(address: String) -> CLLocation? {
var userLocation: CLLocation?
var returnvalue: CLLocation?
let semaphore = dispatch_semaphore_create(0)
CLGeocoder().geocodeAddressString(address, completionHandler: { (placemarks, error) in
if error != nil {
print("Geocoding error: \(error)")
return
}
if placemarks?.count > 0 {
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
dispatch_semaphore_signal(semaphore)
}
}
})
let wait = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
if wait != 0 {
returnvalue = userLocation
}
return returnvalue
}
As Paulw11 already mentioned, a semaphore in this case is very bad programming habit. If you are new to GCD learn to understand the asynchronous pattern for returning received data in a completion block. It's even simpler to handle in Swift than in Objective-C.
This is an example using a completion block:
func forwardGeocoding(address: String, completion: (CLLocation?, NSError?) -> Void) {
CLGeocoder().geocodeAddressString(address, completionHandler: { (placemarks, error) in
if error != nil {
completion(nil, error!)
} else {
if let placemarks = placemarks where !placemarks.isEmpty {
let placemark = placemarks.first!
if let unwrappedLocation = placemark.location {
let coordinate = unwrappedLocation.coordinate
print("Settings location: \(coordinate.latitude), \(coordinate.longitude)")
let CLReadyLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
completion(CLReadyLocation, nil)
}
}
}
})
}
and call it with
forwardGeocoding("foo") { (location, error) in
if error != nil {
print("Geocoding error: \(error!)")
} else {
// do something with the location
}
}
the result of dispatch_semaphore_wait is 0 on success and non zero on timeout occurred. So you should change your code to:
let wait = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
if wait == 0 { //your work is done, without time out.
returnvalue = userLocation //update location
}
return returnvalue //otherwise return nil according to your code above. this code will never execute. In this case, there is no time out cause it wait forever.
Another point, you must call dispatch_semaphore_signal before your block geocodeAddressString end the execution. Otherwise, your application will wait forever in case of getting error.
CLGeocoder().geocodeAddressString(address, completionHandler: { (placemarks, error) in
if error != nil {
print("Geocoding error: \(error)")
dispatch_semaphore_signal(semaphore) //add to here
return
}
if placemarks?.count > 0 {
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
}
}
dispatch_semaphore_signal(semaphore) //and here
})
Finally, when using semaphore that wait forever, you have to make sure that the block of code inside semaphore will end the execution.

Why .geocodeAddressString doesn't change external variable?

I have an external variable chooseCoordinates, which has a type of CLLocationCoordinate2D. This var need to save coordinates from geocodeAddressString closure, but, apparently, it doesn't change.
I would like to ask for advice, how to make this actually closure store the data, so I will be able to parse it to another viewController
var chooseCoordinates = CLLocationCoordinate2D()
////////////
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(sender.text!, completionHandler: { (placemarks, error) -> Void in
if(error != nil) {
print("\(error)")
}
if let placemark = placemarks?.first {
let coordinates: CLLocationCoordinate2D = placemark.location!.coordinate
self.chooseCoordinates = coordinates
}
})
The geocodeAddressString runs asynchronously (i.e. even though the method returns immediately, its completionHandler closure may be called later). So, are you sure it's not changing, or just not changing by the time you try to use chooseCoordinates? You should initiate whatever updating of the UI or whatever from within the closure (or the didSet of chooseCoordinates), not doing so immediately. We don't see how you're using chooseCoordinates, so it's hard to be more specific than that.
For example:
geocoder.geocodeAddressString(sender.text!) { placemarks, error in
if error != nil {
print(error!)
}
if let placemark = placemarks?.first {
let coordinates: CLLocationCoordinate2D = placemark.location!.coordinate
self.chooseCoordinates = coordinates
// call the method that uses `chooseCoordinates` here
}
}
// don't try to use `chooseCoordinates` here, as it hasn't been set yet.
Or, you might employ the completionHandler pattern yourself:
#IBAction func didEndEditingTextField(sender: UITextField) {
geocodeAddressString(sender.text!) { coordinate, error in
self.chooseCoordinates = coordinate
// trigger whatever the next step is here, or in the `didSet` of `chooseCoordinates`
}
// don't try to use `chooseCooordinates` here
}
var chooseCoordinates: CLLocationCoordinate2D? // you probably should make this optional
let geocoder = CLGeocoder()
/// Geocode string
///
/// - parameter string: The string to geocode.
/// - parameter completionHandler: The closure that is called asynchronously (i.e. later) when the geocoding is done.
func geocodeAddressString(string: String, completionHandler: (CLLocationCoordinate2D?, NSError?) -> ()) {
geocoder.geocodeAddressString(string) { placemarks, error in
if error != nil {
print(error!)
}
if let placemark = placemarks?.first {
completionHandler(placemark.location!.coordinate, error)
} else {
completionHandler(nil, error)
}
}
}

Alamofire request gives memory warning

I am using a Master Detail Application. Master Screen is a Dashboard and on selecting an item, moves to the detailed screen where I trigger an Alamofire request in the backend
Below is the snippet
class APIManager: NSObject {
class var sharedManager: APIManager {
return _sharedManager
}
private var requests = [Request]()
// Cancel any ongoing download
func cancelRequests() {
if requests.count > 0 {
for request in requests {
request.cancel()
}
}
}
func getData(completion: (dataSet: [Data]?, error: NSError?) -> Void) {
let request = Alamofire.request(.GET, "http://request")
.response { (request, response, data, error) in
dispatch_async(dispatch_get_main_queue(), {
if(error == nil) {
if let response = data, data = (try? NSJSONSerialization.JSONObjectWithData(response, options: [])) as? [NSDictionary] {
var dataSet = [Data]()
for (_, dictionary) in data.enumerate() {
let lat = dictionary["Latitude"]
let lng = dictionary["Longitude"]
let id = dictionary["ID"] as! Int
let data = Data(lat: lat!, long: lng!, id: shuttleID)
dataSet.append(data)
}
completion(dataSet: dataSet, error: nil)
}
} else { completion(dataSet: nil, error: error) }
})
}
requests.append(request)
}
}
I have a singleton API manager class and from the detail view controller I call getData() function. Everything works fine.
But, when I push and pop repeatedly, I see rapid increase in the memory and after 10-15 attempts, I get memory warning. However in the AppDelegate I am managing it to show an Alert message and adding a delay timer for 8 seconds. But however after 20-25 attempts app crashes due to memory warning.
In viewWillDisappear(), I cancel any ongoing requests also. But I couldn't able to stop memory warning issue. I commented the part where I call the request, I see no issues, even memory consumption is less.
I welcome ideas.
The problem is you are never removing the requests that you append to the member variable 'requests'.
You will need to ensure to remove the request when you either cancel it or when the request completes successfully.
Do the following modifications-
func cancelRequests() {
if requests.count > 0 {
for request in requests {
request.cancel()
}
}
requests.removeAll() //Delete all canseled requests
}
also
func getData(completion: (dataSet: [Data]?, error: NSError?) -> Void) {
let request = Alamofire.request(.GET, "http://request")
.response { (request, response, data, error) in
dispatch_async(dispatch_get_main_queue(), {
if(error == nil) {
if let response = data, data = (try? NSJSONSerialization.JSONObjectWithData(response, options: [])) as? [NSDictionary] {
var dataSet = [Data]()
for (_, dictionary) in data.enumerate() {
let lat = dictionary["Latitude"]
let lng = dictionary["Longitude"]
let id = dictionary["ID"] as! Int
let data = Data(lat: lat!, long: lng!, id: shuttleID)
dataSet.append(data)
}
requests.removeObject(request)
completion(dataSet: dataSet, error: nil)
}
} else {
requests.removeObject(request)
completion(dataSet: nil, error: error) }
})
}
requests.append(request)
}
Add this Handy extension on Array to remove item to your code:
// Swift 2 Array Extension
extension Array where Element: Equatable {
mutating func removeObject(object: Element) {
if let index = self.indexOf(object) {
self.removeAtIndex(index)
}
}
mutating func removeObjectsInArray(array: [Element]) {
for object in array {
self.removeObject(object)
}
}
}
On analysis, I found that the memory warning was not due to the Alamofire request. It was due to MKMapView. Loading a MKMapView, zooming in and zooming out consumes more memory. So, in viewWillDisappear I did the fix.
override func viewWillDisappear(animated:Bool){
super.viewWillDisappear(animated)
self.applyMapViewMemoryFix()
}
func applyMapViewMemoryFix(){
switch (self.mapView.mapType) {
case MKMapType.Hybrid:
self.mapView.mapType = MKMapType.Standard
break;
case MKMapType.Standard:
self.mapView.mapType = MKMapType.Hybrid
break;
default:
break;
}
self.mapView.showsUserLocation = false
self.mapView.delegate = nil
self.mapView.removeFromSuperview()
self.mapView = nil
}
Courtesy - Stop iOS 7 MKMapView from leaking memory

Resources