Domain=kCLErrorDomain Code=8 when fetching location through zipcode - ios

I'm trying to fetch the the latitude and longitude based on the input parameters postal/city and country code. Below is my code, this works fine if enter City and country name but shows error if I enter zipcode and country code. Below is the code. (Note: Location services and app permissions are enabled)
func getLocationFrom(postalCityCode: String, countryCode: String) -> CLLocation? {
let geocoder = CLGeocoder()
var location: CLLocation?
let address = CNMutablePostalAddress()
address.postalCode = postalCityCode
address.country = countryCode
geocoder.geocodePostalAddress(address, preferredLocale: Locale.current) { (placemarks, error) in
guard error == nil else {
print("Error: \(error!)")
return
}
guard let placemark = placemarks?.first else {
print("Error: placemark is nil")
return
}
guard let coordinate = placemark.location?.coordinate else {
print("Error: coordinate is nil")
return
}
location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
print("Found location = \(location)")
}
return location
}
Working input: Shanghai, CN
Failing input: 200040, CN
Edit
Attached updated code as suggested in the answer but still experiencing same issue

Currently, you are using return location before it is set, since geocoder.geocodePostalAddress(...) is an asynchronous function.
That means you need to use a completion handler (for example) to return the location, when it has the results, something like this:
func getLocationFrom(postalCityCode: String, countryCode: String, completion: #escaping ( CLLocation?) -> Void) {
let geocoder = CLGeocoder()
var location: CLLocation?
let address = CNMutablePostalAddress()
address.postalCode = postalCityCode
address.country = countryCode
geocoder.geocodePostalAddress(address, preferredLocale: Locale.current) { (placemarks, error) in
guard error == nil else {
print("Error: \(error!)")
return completion(nil)
}
guard let placemark = placemarks?.first else {
print("Error: placemark is nil")
return completion(nil)
}
guard let coordinate = placemark.location?.coordinate else {
print("Error: coordinate is nil")
return completion(nil)
}
location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
print("\n--> Found location = \(location) \n")
completion(location) // <-- here
}
}
Use it like this:
getLocationFrom(postalCityCode: "200040", countryCode: "CN") { location in
print("\n---> location: \(location) \n")
}
EDIT-1
for testing and isolating the issue, try this code in a new SwiftUI project:
struct ContentView: View {
#State var cityLocation = CLLocation()
var body: some View {
Text(cityLocation.description)
.onAppear {
getLocationFrom(postalCityCode: "200040", countryCode: "CN") { location in
print("\n---> location: \(location) \n")
if let theLocation = location {
cityLocation = theLocation
}
}
}
}
func getLocationFrom(postalCityCode: String, countryCode: String, completion: #escaping ( CLLocation?) -> Void) {
let geocoder = CLGeocoder()
var location: CLLocation?
let address = CNMutablePostalAddress()
address.postalCode = postalCityCode
address.country = countryCode
geocoder.geocodePostalAddress(address, preferredLocale: Locale.current) { (placemarks, error) in
guard error == nil else {
print("Error: \(error!)")
return completion(nil)
}
guard let placemark = placemarks?.first else {
print("Error: placemark is nil")
return completion(nil)
}
guard let coordinate = placemark.location?.coordinate else {
print("Error: coordinate is nil")
return completion(nil)
}
location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
print("\n--> Found location = \(location) \n")
completion(location)
}
}
}

Related

Why does the Array return empty in Swift?

I am trying to add annotations to my MapKit, however the array I append to is returning nil. I used this method before to populate CollectionView in previous ViewControllers and the array had values, now if I print the array it returns 0 values, I am sure that there should be data because It is printed in the console, however not being added to the array. Thoughts please?
My Manager Class:
class GoogleMapsAPIManager {
var bloodBanksArray = [BloodBanksModel]()
func fetchCity(latitude: CLLocationDegrees, longitude: CLLocationDegrees, raduis: Double){
let urlString = "\(K.googleMapsAPI)&location=\(latitude),\(longitude)&radius=\(raduis)&key=\(K.apiKey)"
print(urlString)
performRequest(with: urlString)
}
//MARK: - Decoding JSON
func performRequest(with urlString: String){
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
return
}
if let safeData = data {
self.parseJSON(bloodBankData: safeData)
}
}
task.resume()
}
}
func parseJSON(bloodBankData: Data) {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(BloodBanksData.self, from: bloodBankData)
for i in 0...decodedData.results.count - 1 {
bloodBanksArray.append(BloodBanksModel(name: decodedData.results[i].name, photo: decodedData.results[i].photos?[0].photoReference ?? "ATtYBwJjqXlw3DMdL74SRtgG_GRA3LkAET6hDJXHWtkQMaOTo1B3Gx9jrDTXFLFabGStSxX8BiYdLAnknF7A9ynw33KKyUh5Oc55A9vXzo_6nd4mnk5Sx-iOqMNaw21dk0C424PWaio9GiogQaKecYxOT1q-bmj30syypZmyxkjF7r3-gFyC", open_now: decodedData.results[i].openingHours?.openNow ?? false, vincinity: decodedData.results[i].vicinity, rating: decodedData.results[i].rating, placeId: decodedData.results[i].placeId, lat: decodedData.results[i].geometry.location.lat, lng: decodedData.results[i].geometry.location.lng))
}
print("bossins \(bloodBanksArray)")
} catch {
print(error)
}
}
}
Model Class:
struct BloodBanksModel {
let name: String
let photo: String
let open_now: Bool
let vincinity: String
let rating: Double
let placeId: String
let lat: Double
let lng: Double
}
Data:
import Foundation
// MARK: - BloodBanksData
struct BloodBanksData: Codable {
let results: [Result]
enum CodingKeys: String, CodingKey {
case results
}
}
// MARK: - Result
struct Result: Codable {
let geometry: Geometry
let name: String
let openingHours: OpeningHours?
let photos: [Photo]?
let rating: Double
let vicinity: String
let placeId: String
enum CodingKeys: String, CodingKey {
case geometry, name
case openingHours = "opening_hours"
case photos
case rating
case vicinity
case placeId = "place_id"
}
}
// MARK: - Geometry
struct Geometry: Codable {
let location: Location
}
// MARK: - Location
struct Location: Codable {
let lat, lng: Double
}
// MARK: - OpeningHours
struct OpeningHours: Codable {
let openNow: Bool
enum CodingKeys: String, CodingKey {
case openNow = "open_now"
}
}
// MARK: - Photo
struct Photo: Codable {
let photoReference: String
enum CodingKeys: String, CodingKey {
case photoReference = "photo_reference"
}
}
In my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
//mapView.delegate = self
locationManager.requestWhenInUseAuthorization()
var currentLocation: CLLocation!
if
CLLocationManager.authorizationStatus() == .authorizedWhenInUse ||
CLLocationManager.authorizationStatus() == .authorizedAlways
{
currentLocation = locationManager.location
googleMapsAPIManager.fetchCity(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude, raduis: 100000.0)
}
for location in googleMapsAPIManager.bloodBanksArray {
let annotation = MKPointAnnotation()
annotation.title = location.name
annotation.coordinate = CLLocationCoordinate2D(latitude: location.lat, longitude: location.lng)
annotations.append(annotation)
}
mapView.addAnnotations(annotations)
}
As I said in your previous question fetchCity works asynchronously, you have to add a completion handler
For convenience reasons I merged fetchCity, performRequest and parseJSON() into one method.
func fetchCity(latitude: CLLocationDegrees, longitude: CLLocationDegrees, raduis: Double, completion: #escaping () -> Void){
let urlString = "\(K.googleMapsAPI)&location=\(latitude),\(longitude)&radius=\(raduis)&key=\(K.apiKey)"
print(urlString)
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, _, error) in
if let error = error { print(error); return }
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(BloodBanksData.self, from: data!)
for i in 0..<decodedData.results.count {
bloodBanksArray.append(BloodBanksModel(name: decodedData.results[i].name, photo: decodedData.results[i].photos?[0].photoReference ?? "ATtYBwJjqXlw3DMdL74SRtgG_GRA3LkAET6hDJXHWtkQMaOTo1B3Gx9jrDTXFLFabGStSxX8BiYdLAnknF7A9ynw33KKyUh5Oc55A9vXzo_6nd4mnk5Sx-iOqMNaw21dk0C424PWaio9GiogQaKecYxOT1q-bmj30syypZmyxkjF7r3-gFyC", open_now: decodedData.results[i].openingHours?.openNow ?? false, vincinity: decodedData.results[i].vicinity, rating: decodedData.results[i].rating, placeId: decodedData.results[i].placeId, lat: decodedData.results[i].geometry.location.lat, lng: decodedData.results[i].geometry.location.lng))
}
print("bossins \(bloodBanksArray)")
completion()
} catch {
print(error)
}
}
task.resume()
}
}
And call it
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse ||
CLLocationManager.authorizationStatus() == .authorizedAlways {
currentLocation = locationManager.location
googleMapsAPIManager.fetchCity(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude, raduis: 100000.0) {
for location in googleMapsAPIManager.bloodBanksArray {
let annotation = MKPointAnnotation()
annotation.title = location.name
annotation.coordinate = CLLocationCoordinate2D(latitude: location.lat, longitude: location.lng)
annotations.append(annotation)
}
mapView.addAnnotations(annotations)
}
}
Be aware that Core Location works asynchronously, too.

Use variables inside a completion handler somewhere else

I'm trying to convert address to coordinates in order to create a Firestorm GeoPoint object.
I currently have this code:
func getCoords(from address: String, locationCompletionHandler: #escaping (CLLocationCoordinate2D?, Error?) -> Void) {
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(address) { (placemarks, error) in
guard
let placemarks = placemarks,
let coordinate = placemarks.first?.location?.coordinate
else {
locationCompletionHandler(nil, error)
return
}
locationCompletionHandler(coordinate, nil)
}
}
func addressToGeoPoint(from address: String) -> GeoPoint {
var latitude:Double = 0
var longitude:Double = 0
getCoords(from: address) { coordinate, error in
if let coordinate = coordinate {
latitude = coordinate.latitude
longitude = coordinate.longitude
}
else {
print("Can't get coords: \(String(describing: error?.localizedDescription))")
}
}
return GeoPoint(latitude: latitude, longitude: longitude)
}
The problem is that when the GeoPoint object is being initialized, the latitude and longitude variables are still 0 because the completion handler hasn't finished yet.
The function addressToGeoPoint must return a GeoPoint.
What can I do in order for this to work?
Thanks!

Iterate through JSON array and add coordinates to the map

I'm using an API to get latitude and longitude coordinates and place them on a map with the name of the place it corresponds to. I'm able to put one place's lat and long coordinates but I'm not too sure how to add all of them to a map. I can't get my head around how to do it. I've tried to use a for loop to do it but I'm too sure on how I would implement it. This is what I've got so far:
func getData() {
let url = "https://www.givefood.org.uk/api/2/foodbanks/"
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { [self] data, response, error in
guard let data = data, error == nil else {
print("Wrong")
return
}
var result: [Info]?
do {
result = try JSONDecoder().decode([Info].self, from: data)
}
catch {
print("Failed to convert: \(error.localizedDescription)")
}
guard let json = result else {
return
}
for each in json {
var each = 0
each += 1
let comp = json[each].lat_lng?.components(separatedBy: ",")
let latString = comp![each]
let lonString = comp![each]
let lat = Double(latString)
let lon = Double(lonString)
let locationPin: CLLocationCoordinate2D = CLLocationCoordinate2DMake(lat!, lon!)
let location: CLLocationCoordinate2D = CLLocationCoordinate2DMake(51.55573, -0.108312)
let region = MKCoordinateRegion.init(center: location, latitudinalMeters: regionInMetres, longitudinalMeters: regionInMetres)
mapView.setRegion(region, animated: true)
let myAn1 = MapPin(title: json[each].name!, locationName: json[each].name!, coordinate: locationPin)
mapView.addAnnotations([myAn1])
}
})
task.resume()
}
Your loop is wrong, each after for is one Info item. The Int index each is pointless and you set it in each iteration to zero so you get always the same coordinate (at index 1).
First of all declare name and lat_lng as non-optional. All records contain both fields.
struct Info : Decodable {
let lat_lng : String
let name : String
}
Second of all for convenience reasons extend CLLocationCoordinate2D to create a coordinate from a string
extension CLLocationCoordinate2D {
init?(string: String) {
let comp = string.components(separatedBy: ",")
guard comp.count == 2, let lat = Double(comp[0]), let lon = Double(comp[1]) else { return nil }
self.init(latitude: lat, longitude: lon )
}
}
Third of all put all good code into the do scope instead of dealing with optionals and set the region once before the loop
func getData() {
let url = "https://www.givefood.org.uk/api/2/foodbanks/"
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { [self] data, response, error in
if let error = error { print(error); return }
do {
let result = try JSONDecoder().decode([Info].self, from: data!)
let location = CLLocationCoordinate2D(latitude: 51.55573, longitude: -0.108312)
let region = MKCoordinateRegion.init(center: location, latitudinalMeters: regionInMetres, longitudinalMeters: regionInMetres)
mapView.setRegion(region, animated: true)
var pins = [MapPin]()
for info in result {
if let coordinate = CLLocationCoordinate2D(string: info.lat_lng) {
pins.append(MapPin(title: info.name, locationName: info.name, coordinate: coordinate))
}
}
DispatchQueue.main.async {
self.mapView.addAnnotations(pins)
}
}
catch {
print("Failed to convert: \(error)")
}
})
task.resume()
}

Unable to get city name by current latitude and longitude in swift

I'm trying to get city name from my current location coordiate by using CLGeocoder().reverseGeocodeLocation.
It gives me country name, street name, state and many other things but not city. Is there anything wrong with my code?
Here's my code:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations[0]
CLGeocoder().reverseGeocodeLocation(location) { (placeMark, error) in
if error != nil{
print("Some errors: \(String(describing: error?.localizedDescription))")
}else{
if let place = placeMark?[0]{
print("country: \(place.administrativeArea)")
self.lblCurrentLocation.text = place.administrativeArea
}
}
} }
I use the below code too. But doesn't work for me. Here's the another way.
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: (self.locationManager.location?.coordinate.latitude)!, longitude: (self.locationManager.location?.coordinate.longitude)!)
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
// Place details
var placeMark: CLPlacemark!
placeMark = placemarks?[0]
// Address dictionary
print(placeMark.addressDictionary as Any)
// Location name
if let locationName = placeMark.addressDictionary!["Name"] as? NSString {
print("locationName: \(locationName)")
}
// Street address
if let street = placeMark.addressDictionary!["Thoroughfare"] as? NSString {
print("street: \(street)")
}
// City
if let city = placeMark.addressDictionary!["City"] as? NSString {
print("city : \(city)")
}
// Zip code
if let zip = placeMark.addressDictionary!["ZIP"] as? NSString {
print("zip :\(zip)")
}
// Country
if let country = placeMark.addressDictionary!["Country"] as? NSString {
print("country :\(country)")
}
})
Please someone help me to get city name.
The field is called locality
if let locality = placeMark.addressDictionary!["locality"] as? NSString {
print("locality :\(locality)")
}
Locality Apple docs
https://developer.apple.com/documentation/corelocation/clplacemark/1423507-locality?language=objc
CLPlacemark
https://developer.apple.com/documentation/corelocation/clplacemark?language=objc
Update:
Try this
import Foundation
import CoreLocation
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: 40.730610, longitude: -73.935242) // <- New York
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, _) -> Void in
placemarks?.forEach { (placemark) in
if let city = placemark.locality { print(city) } // Prints "New York"
}
})

Having trouble retrieving data from CloudKit

I am having trouble fetching the locations from cloudkit. The location gets uploaded, but when I try to have them printed out and loaded, they aren't downloaded. I don't get any errors.
This function uploads the location to CloudKit:
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
let location = locations.last
let center = CLLocationCoordinate2D(latitude: location!.coordinate.latitude, longitude: location!.coordinate.longitude)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.015, longitudeDelta: 0.015))
self.mapView.setRegion(region, animated: true)
self.locationManager.stopUpdatingLocation()//
let locationRecord = CKRecord(recordType: "location")
locationRecord.setObject(location, forKey: "location")
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(locationRecord) { record, error in
}
if error == nil
{
print("Location saved")
}
event1 = locations
}
This function fetches the locations from CloudKit:
func loadLocation()
{
let locations = [CKRecord]()
let publicData1 = CKContainer.defaultContainer().publicCloudDatabase
let query1 = CKQuery(recordType: "location", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray:nil))
publicData1.performQuery(query1, inZoneWithID: nil) { (results: [CKRecord]?, error: NSError?) -> Void in
if let locations = results
{
self.locations = locations
print(locations)
}
}
}
So to do this I made a unit test, that passes:
//
// CloudKitLocationsTests.swift
//
import XCTest
import UIKit
import CoreLocation
import CloudKit
class CloudKitLocationsTests: XCTestCase {
let locations = [ CLLocation(latitude: 34.4, longitude: -118.33), CLLocation(latitude: 32.2, longitude: -121.33) ]
func storeLocationToCloud(location:CLLocation) {
let locationRecord = CKRecord(recordType: "location")
locationRecord.setObject(location, forKey: "location")
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(locationRecord) { (records, error) in
if error != nil {
print("error saving locations: \(error)")
} else {
print("Locations saved: \(records)")
}
}
}
func fetchLocationsFromCloud(completion: (error:NSError?, records:[CKRecord]?) -> Void) {
let query = CKQuery(recordType: "Location", predicate: NSPredicate(value: true))
CKContainer.defaultContainer().publicCloudDatabase.performQuery(query, inZoneWithID: nil){
(records, error) in
if error != nil {
print("error fetching locations")
completion(error: error, records: nil)
} else {
print("found locations: \(records)")
completion(error: nil, records: records)
}
}
}
func testSavingLocations(){
let testExpectation = expectationWithDescription("saveLocations")
var n = 0
for location in self.locations {
let locationRecord = CKRecord(recordType: "Location")
locationRecord["location"] = location
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(locationRecord) { (records, error) in
if error != nil {
print("error saving locations: \(error)")
} else {
print("Locations saved: \(records)")
}
n += 1
if n >= self.locations.count {
testExpectation.fulfill()
}
}
}
// do something then call fulfill (in callback)
waitForExpectationsWithTimeout(10){ error in
if error != nil {
XCTFail("timed out waiting on expectation: \(testExpectation)")
}
}
}
func testFetchingLocations(){
let testExpectation = expectationWithDescription("FetchLocations")
fetchLocationsFromCloud(){ (error, records) in
if error != nil {
XCTFail("error fetching locations")
} else {
XCTAssertGreaterThan(records!.count, 0)
}
// do something then call fulfill (in callback)
testExpectation.fulfill()
}
waitForExpectationsWithTimeout(10){ error in
if error != nil {
XCTFail("timed out waiting on expectation: \(testExpectation)")
}
}
}
}
Note that you had case mismatch Location/location. Also, I am doing a subscript to set the field value.
Run this it works. Getting the location from the location manger callback has nothing to do with CloudKit so you should be able to plug this in as you require.
One other thing: I did turn on the option to allow you to query on ID field for the Location record type.
If your problem is to retrieve an array of CLLocation, try this:
publicData1.performQuery(query1, inZoneWithID: nil) { records, error in
var locations = [CLLocation]()
if let records = records {
for record in records {
if let location = record["location"] as? CLLocation {
locations.append(location)
}
}
}
}

Resources