Swift CLGeocoder to get TimeZone

553 views Asked by At

I'm afraid I likely have the completion handler all messed up. What I am trying to do is use latitude and longitude to get a TimeZone. I want the entire function to return the value with a type TimeZone. The following code works, as in I can get the correct timeZone but it's at the returning stage that it falls apart.

func getTimeZone(location: CLLocationCoordinate2D, completion: () -> TimeZone) -> TimeZone {
    
    var timeZone = TimeZone(identifier: "timeZone")
    var placemark: CLPlacemark?
    let cllLocation = CLLocation(latitude: location.latitude, longitude: location.longitude)
    let geocoder = CLGeocoder()
    
    geocoder.reverseGeocodeLocation(cllLocation) { placemarks, error in
        
        if let error = error {
            print(error.localizedDescription)
        } else {
            
            if let placemarks = placemarks {
                placemark = placemarks.first
            }
        }
        DispatchQueue.main.async {
            
            if let optTime = placemark?.timeZone {
                timeZone = optTime
            }

            return completion()
        }
    }
}
4

There are 4 answers

2
Francisco Cordoba On BEST ANSWER

I think there is a problem with the completion implementation. Try to change it to something like this:

func getTimeZone(location: CLLocationCoordinate2D, completion: @escaping ((TimeZone) -> Void)) {
    let cllLocation = CLLocation(latitude: location.latitude, longitude: location.longitude)
    let geocoder = CLGeocoder()

    geocoder.reverseGeocodeLocation(cllLocation) { placemarks, error in

        if let error = error {
            print(error.localizedDescription)

        } else {
            if let placemarks = placemarks {
                if let optTime = placemarks.first!.timeZone {
                    completion(optTime)
                }
            }
        }
    }
}

Then you can call the function like this:

getTimeZone(location: CLLocationCoordinate2D(latitude: CLLocationDegrees(9.62), longitude: CLLocationDegrees(84.62))) { timeZone in
    print("Time zone: \(timeZone)")
}
0
Patratacus On

Xcode 14, Swift 5.7

CLLocationCoordinate2D is causing problem in Xcode 14. if you runt the following code in a new playground or in a Xcode 14 project:

import CoreLocation

print("Before")
let center = CLLocationCoordinate2D()
print("After")

This will immediately crash playground or halt your code due to this error:

libc++abi: terminating with uncaught exception of type NSException

I have example this working on Xcode 14.0 SwiftUI project and Playground by using CLLocation and not CLLocationCoordinate2D:

import CoreLocation
func getTimeZone(location: CLLocation, completion: @escaping ((TimeZone) -> Void)) {
   
    let geocoder = CLGeocoder()

    geocoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) in
        if error != nil {
            print("reverse geodcode fail: \(String(describing: error))")
        } else if let placemarks = placemarks, let pm = placemarks.first {
            
            if let val = pm.timeZone {
                completion(val)
            }
        }
    })
}

getTimeZone(location: CLLocation(latitude: 42.48001070918, longitude: -76.4511703657)) { timeZone in
    print("My Time zone: \(timeZone.identifier)")
}

Output: My Time zone: America/New_York

Here's a variation (based on a modified answer for this post: Running reverseGeocodeLocation function create exception in SwiftPlay or return nothing) if you want to look up latitude and longitude using string and return all the other information from the location:

import CoreLocation

func getAddressFromLatLon(pdblLatitude: String, withLongitude pdblLongitude: String) {
    guard let lat = Double(pdblLatitude),
          let lon = Double(pdblLongitude) else { return }

    let loc = CLLocation(latitude: lat, longitude: lon)
    let ceo = CLGeocoder()

    ceo.reverseGeocodeLocation(loc, completionHandler: { (placemarks, error) in
        if error != nil {
            print("reverse geodcode fail: \(String(describing: error))")
        } else if let placemarks = placemarks, let pm = placemarks.first {
            var addressString = ""
            if let val = pm.subLocality {
                addressString += val + ", "
            }
            if let val = pm.thoroughfare {
                addressString += val + ", "
            }
            if let val = pm.locality {
                addressString += val + ", "
            }
            if let val = pm.country {
                addressString += val + ", "
            }
            if let val = pm.postalCode {
                addressString += val + " "
            }

            print(addressString)
        }
    })
}
getAddressFromLatLon(pdblLatitude: "42.48001070918", withLongitude: "-76.4511703657")

Output: Sapsucker Woods Rd, Lansing, United States, 14850

0
Sandeep Bhandari On

Why are you trying to return a value when you have async reverseGeocodeLocation involved in getTimeZone

func getTimeZone(location: CLLocationCoordinate2D, completion: @escaping (TimeZone) -> ()) {
        var placemark: CLPlacemark?
        let cllLocation = CLLocation(latitude: location.latitude, longitude: location.longitude)
        let geocoder = CLGeocoder()

        geocoder.reverseGeocodeLocation(cllLocation) { placemarks, error in

            if let error = error {
                print(error.localizedDescription)
            } else {

                if let placemarks = placemarks {
                    placemark = placemarks.first
                }
            }
            DispatchQueue.main.async {

                if let optTime = placemark?.timeZone {
                    completion(optTime)
                }
            }
        }
    }

Finally use it as

        getTimeZone(location: your_location_coordinates_here) {[weak self] timeZone in
            debugPrint(timeZone)
        }
3
Duncan C On

You've got it almost right. Get rid of the return value. You can't return a function result from an async function like that.

Instead the caller passes in a completion handler that executes when the result is returned:

func getTimeZone(location: CLLocationCoordinate2D, completion:  @escaping (TimeZone) -> ()) {
    
    var timeZone = TimeZone(identifier: "timeZone")
    var placemark: CLPlacemark?
    let cllLocation = CLLocation(latitude: location.latitude, longitude: location.longitude)
    let geocoder = CLGeocoder()
    
    geocoder.reverseGeocodeLocation(cllLocation) { placemarks, error in
        
        if let error = error {
            print(error.localizedDescription)
        } else {
            
            if let placemarks = placemarks {
                placemark = placemarks.first
            }
        }
        DispatchQueue.main.async {
            
            if let optTime = placemark?.timeZone {
                timeZone = optTime
            }
            completion(timeZone)
        }
    }
}

Then, to use it:

getTimeZone(location: someLocation) { timeZone in
   // This code will execute once the time zone is calculated.
   print("The time zone for that location is \(timeZone.description)")
}