I'm using a CLGeoCoder instance to get the city and country of a given set of coordinates (latitude and longitude) through its method reverseGeocodeLocation().
The documentation specifies that this data should be in CLPlacemark's country and locality properties but the results only seem to be inside addressDictionary property which was deprecated since iOS 11.
Image of debugging instance showing addressDictionary present but neither country nor locality
This therefore works but shows this warning on all three uses of addressDictionary:
'addressDictionary' was deprecated in iOS 11.0: Use #properties
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location, preferredLocale: nil) { (placemarksArray, error) in
if let error = error {
print(error)
} else {
if let placemark = placemarksArray?.last {
var city = ""
if let ct = placemark.addressDictionary?["City"] {
city = "\(ct), "
}
var state = ""
if let st = placemark.addressDictionary?["State"] {
state = "\(st), "
}
var country = ""
if let cot = placemark.addressDictionary?["Country"] {
country = "\(cot)"
}
self.cityCountry = "\(city)\(state)\(country)"
}
}
}
I tried referencing the properties but they just return nil values.
Am I calling the wrong method or is this a bug?
Suddenly references to the country, locality and subAdministrativeArea started working! Couldn't figure out what caused them not to work earlier though.
Related
I have a Google Firebase Cloud Function that returns an array of items. In my iOS app, I need to fetch the returned array and append it into another array in my Swift code.
Here's what I've got so far in Swift:
struct Item: Identifiable {
let id: UUID
let user: String
let name: String
let icon: String
let latitude: Double
let longitude: Double
init(id: UUID = UUID(), user: String, name: String, icon: String, latitude: Double, longitude: Double) {
self.id = id
self.user = user
self.name = name
self.icon = icon
self.latitude = latitude
self.longitude = longitude
}
}
#State var items = [Item]() // This is the array that I need to put the returned data into.
Functions.functions().httpsCallable("getItems").call() { result, error in
// This is where I'm stuck. I need to put the items from the result into the items array.
}
This is what the result?.data equals:
Optional({
items = (
{
icon = snow;
latitude = "39.13113";
longitude = "-84.518387";
name = Clothing;
user = tS7T8ATGCLTZOXi3ZGr0iaWWJAf1;
},
{
icon = eyeglasses;
latitude = "37.785834";
longitude = "-122.406417";
name = Glasses;
user = tS7T8ATGCLTZOXi3ZGr0iaWWJAf1;
},
{
icon = "wallet.pass";
latitude = "37.785834";
longitude = "-122.406417";
name = Wallet;
user = tS7T8ATGCLTZOXi3ZGr0iaWWJAf1;
},
{
icon = key;
latitude = "37.785834";
longitude = "-122.406417";
name = Keys;
user = tS7T8ATGCLTZOXi3ZGr0iaWWJAf1;
},
{
icon = laptopcomputer;
latitude = "37.785834";
longitude = "-122.406417";
name = "Laptop/Tablet";
user = tS7T8ATGCLTZOXi3ZGr0iaWWJAf1;
},
{
icon = iphone;
latitude = "37.785834";
longitude = "-122.406417";
name = Phone;
user = tS7T8ATGCLTZOXi3ZGr0iaWWJAf1;
}
);
})
Firebase Function:
exports.getItems = functions.https.onCall(async (data, context) => {
let resultMessage;
let items = [];
if (context.auth.uid) {
const itemsRef = db.collection('items');
const itemsRes = await itemsRef.orderBy('timestamp', 'desc').limit(255).get();
if (!itemsRes.empty) {
itemsRes.forEach(itemDoc => {
const item = {
user: itemDoc.data().user,
name: itemDoc.data().name,
icon: itemDoc.data().icon,
latitude: itemDoc.data().latitude,
longitude: itemDoc.data().longitude
}
items.push(item);
});
resultMessage = "Successfully got the items.";
} else {
resultMessage = "Failed to get the items because there are no items to get.";
}
} else {
resultMessage = "Failed to get the items because the user does not have an ID.";
}
functions.logger.log(resultMessage);
return {
items: items
};
});
I feel like this should be super easy but am still very new to Swift. I read Google Firebase's Documentation and figured out how to get a single variable but not an array. I have also been searching the internet and have had no luck finding a solution. Any help would be greatly appreciated!!
To answer your question directly, your cloud function is returning a native JavaScript object which you need to properly cast to a native Swift object. Your cloud function actually returns a "dictionary" (Object in JavaScript) that only contains an array so I would ditch this pattern and just return the array (otherwise you'll have to just unpack it again on the client and that's redundant). Therefore, in your cloud function, just return the array:
return items;
This array will be packaged as an NSArray on the Swift client in the result's data property. Therefore, simply cast it as such. Each item in that array is then packaged as an NSDictionary which you can treat as a Swift [String: Any] dictionary.
Functions.functions().httpsCallable("getItems").call() { result, error in
// Cast your data as an array of Any
guard let items = result?.data as? [Any] else {
if let error = error {
print(error)
}
return // forgot to add this!
}
// Iterate through your array and cast each iteration as a [String: Any] dictionary
for item in items {
if let item = item as? [String: Any],
let name = item["name"] as? String,
let user = item["user"] as? String,
let icon = item["icon"] as? String,
let latitude = item["latitude"] as? Double,
let longitude = item["longitude"] as? Double {
print(name)
let item = Item(name: name...) // instantiate
items.append(item) // append
}
}
}
There are much more attractive ways of parsing this data. You can map the array, integrate the type casting in the loop, use Firestore's SwiftUI API which is just a couple lines of code, etc.
If, however, you insist on returning a "dictionary" from the cloud function then just modify the guard statement on the client and cast it appropriately:
guard let items = result?.data as? [String: Any] else {
return
}
And then get the items value from the dictionary which would be of type [Any] and you're off to the races.
One last thing, on the server side, I would consider using a try-catch block to handle errors since you are already using the async-await pattern, which I think is a great idea. Remember that these cloud functions must be terminated by either returning a Promise or throwing a Firebase-specific HTTPS error. You don't do any error handling in your cloud function which I would consider problematic.
I want to fetch full address by latitude and longitude in here map iOS premium sdk.In Android, I see there is the option to fetch address by latitude and longitude with ReverseGeocodeRequest but I did not find anything for iOS.
Currently, I am fetching the address from CLLocationCoordinate2D but I think it will be better if I will fetch it by HERE MAP sdk because I am using HERE MAP not Apple MAP. I have attached the android code below.
GeoCoordinate vancouver = new GeoCoordinate(latitude,longitude);
new ReverseGeocodeRequest(vancouver).execute(new ResultListener<Location>() {
#Override
public void onCompleted(Location location, ErrorCode errorCode) {
try {
assetData.address = location.getAddress().toString().replace("\n", "");
} catch (NullPointerException e) {
e.printStackTrace();
}
}
});
You have to make use of NMAAddress class in the HERE iOS SDK.
NMAAddress provides textual address information including house number, street name, city, country, district and more. It encompasses everything about an address or a point on the map. The NMAPlaceLocation class represents an area on the map where additional attributes can be retrieved. These additional attributes include NMAAddress, unique identifier, label, location, access locations, and NMAGeoBoundingBox for the location
Please check the document section Geocoding and Reverse Geocoding for more details including sample codes.
You need to use Google API to fetch address from Geo Coordinates, for that please use following code
func getAddressFromLatLong(latitude: Double, longitude : Double) {
let url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=\(latitude),\(longitude)&key=YOUR_API_KEY_HERE"
Alamofire.request(url).validate().responseJSON { response in
switch response.result {
case .success:
let responseJson = response.result.value! as! NSDictionary
if let results = responseJson.object(forKey: "results")! as? [NSDictionary] {
if results.count > 0 {
if let addressComponents = results[0]["address_components"]! as? [NSDictionary] {
self.address = results[0]["formatted_address"] as? String
for component in addressComponents {
if let temp = component.object(forKey: "types") as? [String] {
if (temp[0] == "postal_code") {
self.pincode = component["long_name"] as? String
}
if (temp[0] == "locality") {
self.city = component["long_name"] as? String
}
if (temp[0] == "administrative_area_level_1") {
self.state = component["long_name"] as? String
}
if (temp[0] == "country") {
self.country = component["long_name"] as? String
}
}
}
}
}
}
case .failure(let error):
print(error)
}
}
}
Im using a function to return a value from Firebase, I'm doing this in a class called DBProvider and ultimately, what I'm trying to do is retrieve a value (latitude) from Firebase and return it in the function (getUsersLatitude()). What I attempted to do was to declare a variable in the function and set it to the value retrieved from Firebase (as seen below):
class DBProvider {
static let _instance = DBProvider()
//private init() {}
static var Instance: DBProvider {
return _instance
}
var dbRef: FIRDatabaseReference {
return FIRDatabase.database().reference()
}
let ref = FIRDatabase.database().reference()
func getUsersLatitude() -> CLLocationDegrees{
var latitude = CLLocationDegrees()
ref.child("users").child(FIRAuth.auth()!.currentUser!.uid).child("location").observeSingleEvent(of: .value, with: { (snapshot) in
if let loc = snapshot.value as? [String : AnyObject]{
if let lat = loc["latitude"]{
latitude = lat as! CLLocationDegrees
print("LATITUDE - \(latitude)")
}
}
})
ref.removeAllObservers()
print("OUTSIDE LATITUDE - \(latitude)")
return latitude
}
}
I call this function in another class called PostViewController, like so:
override func viewDidLoad() {
if FIRAuth.auth()?.currentUser != nil{
DBProvider.Instance.getUsersLatitude()
}
}
Although, the problem that im having is that it does not return latitude, it returns 0.0. I know this because of the print statements -- print("LATITUDE - \(latitude)"), which is the one inside ref..., comes out with the correct latitude, 55.633348.
Whereas, print("OUTSIDE LATITUDE - \(latitude)"), which is the one outside ref..., does not come out with the correct latitude, 0.0.
When if filter for the word "LATITUDE" in the debugger, this is the output I get:
Any help would be greatly appreciated, thank you!
Here's the code:
CLGeocoder().reverseGeocodeLocation(myCurrentLoc) { (array, err) in
print(err)
if err != nil {
print(err)
return
}
if let place = array?.first {
if let locality = place.locality {
if let country = place.country {
if let addr = place.addressDictionary {
print(addr)
}
let myAddress = "\(locality) \(country)"
self.adress.text = myAddress
self.locMgr.stopUpdatingLocation()
}
}
}
self.locMgr.stopUpdatingLocation()
}
So I can read longitude/latitude, and the only thing I need is the full address. Right now I can print out "New York, US", I find all the information I need is in place.addressDictionary, which is a dictionary. But I can't seem to read anything from that dictionary using normal syntax. How do I do it?
For US address you can use this:
let address = [
place.name,
place.locality,
place.administrativeArea,
place.postalCode,
place.country
]
let formattedAddress = address.flatMap({ $0 }).joined(separator: ", ")
Address is an array of components that are used to compose typical postal address. It can contain nil for some items.
FormattedAddress removes the nils and adds delimiter, I used coma here.
I usually make a location manager helper to fetch details and use a placemark
The dictionary you are talking about comes with placemarks
Here is my Model
class UserLocation: NSObject {
var latitude : String?
var longitude: String?
var timeZone: String?
var city : String?
var state : String?
var country : String?
// you can and more fields of placemark
required init(latitude:String?,longitude:String?,timezone:String?,city:String?,state:String?,country:String?) {
self.latitude = latitude
self.longitude = longitude
self.timeZone = timezone
self.city = city
self.state = state
self.country = country
}
}
And then I use the below code to reverse geocode after getting a location from Location Manager and use placemarks to get details
//Location Manager delegate
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
manager.stopUpdatingLocation()
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)->Void in
if (error != nil) {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
if placemarks!.count > 0 {
let pm = placemarks![0]
if let location = manager.location {
let userLoc = UserLocation(latitude: String(location.coordinate.latitude), longitude: String(location.coordinate.longitude), timezone: NSTimeZone.localTimeZone().name, city: pm.locality!, state: pm.administrativeArea!, country:pm.country!)
//By printing this dictionary you will get the complete address
print(pm.addressDictionary)
print(userLoc.city!)
print(userLoc.state!)
print(userLoc.country!)
print(userLoc.latitude!)
print(userLoc.longitude!)
print(userLoc.timeZone!)
} else {
//Handle error
}
if(!CLGeocoder().geocoding){
CLGeocoder().cancelGeocode()
}
}else{
print("Problem with the data received from geocoder")
}
}
Also you can fetch other properties of placemark like
I also printed the addressDictionary and you can see the complete address in the console in the last image
I want to format CLPlacemark to string.
The well known way is to use ABCreateStringWithAddressDictionary but it was deprecated in iOS 9. Warning tells me to use CNPostalAddressFormatter instead.
However, CNPostalAddressFormatter can only format CNPostalAddress. There is no way to properly convert CLPlacemark to CNPostalAddress; only these 3 properties are shared by CLPlacemark and CNPostalAddress: country, ISOcountryCode, and postalCode.
So how should I format CLPlacemark to string now?
Take the placemark's addressDictionary and use its "FormattedAddressLines" key to extract the address string. Note that this is an array of the lines of the string.
(You are correct, however, that the Apple developers tasked with converting to the Contacts framework seem to have forgotten completely about the interchange between Address Book and CLPlacemark. This is a serious bug in the Contacts framework - one of many.)
EDIT Since I posted that answer originally, Apple fixed this bug. A CLPlacemark now has a postalAddress property which is a CNPostalAddress, and you can then use a CNPostalAddressFormatter to get a nice multi-line address string. Be sure to import Contacts!
Swift 3.0
if let lines = myCLPlacemark.addressDictionary?["FormattedAddressLines"] as? [String] {
let placeString = lines.joined(separator: ", ")
// Do your thing
}
Swift 4.1 (and 3 & 4, save 1 line)
I read the question to ask 'How might I implement this?':
extension String {
init?(placemark: CLPlacemark?) {
// Yadda, yadda, yadda
}
}
Two Methods
I first went for porting the AddressDictionary method, as did other posters. But that means losing the power and flexibility of the CNPostalAddress class and formatter. Hence, method 2.
extension String {
// original method (edited)
init?(depreciated placemark1: CLPlacemark?) {
// UPDATE: **addressDictionary depreciated in iOS 11**
guard
let myAddressDictionary = placemark1?.addressDictionary,
let myAddressLines = myAddressDictionary["FormattedAddressLines"] as? [String]
else { return nil }
self.init(myAddressLines.joined(separator: " "))
}
// my preferred method - let CNPostalAddressFormatter do the heavy lifting
init?(betterMethod placemark2: CLPlacemark?) {
// where the magic is:
guard let postalAddress = CNMutablePostalAddress(placemark: placemark2) else { return nil }
self.init(CNPostalAddressFormatter().string(from: postalAddress))
}
}
Wait, what is that CLPlacemark → CNPostalAddress initializer??
extension CNMutablePostalAddress {
convenience init(placemark: CLPlacemark) {
self.init()
street = [placemark.subThoroughfare, placemark.thoroughfare]
.compactMap { $0 } // remove nils, so that...
.joined(separator: " ") // ...only if both != nil, add a space.
/*
// Equivalent street assignment, w/o flatMap + joined:
if let subThoroughfare = placemark.subThoroughfare,
let thoroughfare = placemark.thoroughfare {
street = "\(subThoroughfare) \(thoroughfare)"
} else {
street = (placemark.subThoroughfare ?? "") + (placemark.thoroughfare ?? "")
}
*/
city = placemark.locality ?? ""
state = placemark.administrativeArea ?? ""
postalCode = placemark.postalCode ?? ""
country = placemark.country ?? ""
isoCountryCode = placemark.isoCountryCode ?? ""
if #available(iOS 10.3, *) {
subLocality = placemark.subLocality ?? ""
subAdministrativeArea = placemark.subAdministrativeArea ?? ""
}
}
}
Usage
func quickAndDirtyDemo() {
let location = CLLocation(latitude: 38.8977, longitude: -77.0365)
CLGeocoder().reverseGeocodeLocation(location) { (placemarks, _) in
if let address = String(depreciated: placemarks?.first) {
print("\nAddress Dictionary method:\n\(address)") }
if let address = String(betterMethod: placemarks?.first) {
print("\nEnumerated init method:\n\(address)") }
}
}
/* Output:
Address Dictionary method:
The White House 1600 Pennsylvania Ave NW Washington, DC 20500 United States
Enumerated init method:
1600 Pennsylvania Ave NW
Washington DC 20500
United States
*/
Whoever read until here gets a free T-shirt. (not really)
*This code works in Swift 3 & 4, except that flatMap for removing nil values has been depreciated/renamed to compactMap in Swift 4.1 (Doc here, or see SE-187 for the rationale).
Swift 3.0 Helper Method
class func addressFromPlacemark(_ placemark:CLPlacemark)->String{
var address = ""
if let name = placemark.addressDictionary?["Name"] as? String {
address = constructAddressString(address, newString: name)
}
if let city = placemark.addressDictionary?["City"] as? String {
address = constructAddressString(address, newString: city)
}
if let state = placemark.addressDictionary?["State"] as? String {
address = constructAddressString(address, newString: state)
}
if let country = placemark.country{
address = constructAddressString(address, newString: country)
}
return address
}