I am trying to make a fetch for places in a certain, small bounds that match a filter with no string search doing the following:
let filter = GMSAutocompleteFilter()
filter.type = .establishment
let startCoord = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let upperCoord = startCoord.translate(using: -7, longitudinalMeters: 7)
let lowerCoord = startCoord.translate(using: 7, longitudinalMeters: -7)
let bounds = GMSCoordinateBounds(coordinate: upperCoord, coordinate: lowerCoord)
GMSPlacesClient.shared().autocompleteQuery(
"",
bounds: bounds,
filter: filter,
callback: {
(results, error) in
if error != nil {
self.proxyView!.errorLbl.displayMessage(FAUErrorLabel.MessageLevel.error, message: RQTText.errorsAPIFail)
}
else{
print(results)
print("done")
}
}
)
However, if the string query is empty, the request fails. How can I search places with the ios SDK without a search string?
Related
I am having trouble with working over data I receive from server.
Each time server sends data it is new coordinates for each user. I am looping over each incoming data, and I want to send the data in completion to receive them on other end. And update model class with them. At the moment I have two users in server. And sometimes the closure passes data two times, but sometimes just one. And interesting thing is that class properties are not updated, at least I dont see them on UI.
This is function I call when data is received. Response is just string I split to get user data.
func updateUserAdrress(response: String, completion: #escaping (Int, Double, Double, String) -> Void){
var data = response.components(separatedBy: "\n")
data.removeLast()
data.forEach { part in
let components = part.components(separatedBy: ",")
let userID = components[0].components(separatedBy: " ")
let id = Int(userID[1])
let latitude = Double(components[1])!
let longitude = Double(components[2])!
let location = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if error == nil {
let placemark = placemarks?[0]
if let thoroughfare = placemark?.thoroughfare, let subThoroughfare = placemark?.subThoroughfare {
let collectedAddress = thoroughfare + " " + subThoroughfare
DispatchQueue.main.async {
completion(id!, latitude, longitude, collectedAddress)
}
}
} else {
print("Could not get address \(error!.localizedDescription)")
}
}
}
}
In this function I try to invoke the changes on objects. As the incoming data from server is different the first time I have splited the functionality, so the correct block of code would be called.
func collectUsers(_ response: String){
if users.count != 0{
updateUserAdrress(response: response) { id, latitude, longitude, address in
if let index = self.users.firstIndex(where: { $0.id == id }){
let user = self.users[index]
user.latitude = latitude
user.longitude = longitude
user.address = address
}
}
}else{
var userData = response.components(separatedBy: ";")
userData.removeLast()
let users = userData.compactMap { userString -> User? in
let userProperties = userString.components(separatedBy: ",")
var idPart = userProperties[0].components(separatedBy: " ")
if idPart.count == 2{
idPart.removeFirst()
}
guard userProperties.count == 5 else { return nil }
guard let id = Int(idPart[0]),
let latitude = Double(userProperties[3]),
let longitude = Double(userProperties[4]) else { return nil }
let collectedUser = User(id: id, name: userProperties[1], image: userProperties[2], latitude: latitude, longitude: longitude)
return collectedUser
}
DispatchQueue.main.async {
self.users = users
}
}
}
As I also need user address when app starts in model I have made simular function to call in init so it would get address for user. That seems to be working fine. But for more context I will add the model to.
class User: Identifiable {
var id: Int
let name: String
let image: String
var latitude: Double
var longitude: Double
var address: String = ""
init(id: Int, name: String, image: String, latitude: Double, longitude: Double){
self.id = id
self.name = name
self.image = image
self.latitude = latitude
self.longitude = longitude
getAddress(latitude: latitude, longitude: longitude) { address in
self.address = address
}
}
func getAddress(latitude: Double, longitude: Double, completion: #escaping (String) -> Void){
let location = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if error == nil {
let placemark = placemarks?[0]
if let thoroughfare = placemark?.thoroughfare, let subThoroughfare = placemark?.subThoroughfare {
let collectedAddress = thoroughfare + " " + subThoroughfare
completion(collectedAddress)
}
} else {
print("Could not get address \(error!.localizedDescription)")
}
}
}
}
And one interesting thing. That when closure receives two times data and I assign them to class, there are no changes on UI.
I made this project on Xcode 13.4.1 because on Xcode 14 there is a bug on MapAnnotations throwing purple warnings on view changes.
I need to fetch all users within x radius that match the preferences of the current user.
So I started implementing geohash from Firebase, the documentation was great, my problem might be silly but I don't know when to use all my compound queries...(whereField)
I'm specifying them in the "for query in queries" below but I have the feeling It's not the right spot. how do you use compound queries when using geohash?
//MARK: - GET MATCHES WITH GEOHASH
func getMatchesNearMe(radius: Double) {
// Find matches within 50km of my location
let user = UserService.shared.user
let center = CLLocationCoordinate2D(latitude: user.latitude ?? 0, longitude: user.longitude ?? 0)
let radiusInKilometers: Double = radius
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
withRadius: radiusInKilometers)
let queries = queryBounds.compactMap { (any) -> Query? in
guard let bound = any as? GFGeoQueryBounds else { return nil }
return db.collection("users")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
}
var matchingDocs = [Matches]()
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
return
}
print("\nDocs: Count \(documents.count)")
for doc in snapshot!.documents {
var m = Matches()
m.latitude = doc.data()["latitude"] as? Double ?? 0
m.longitude = doc.data()["longitude"] as? Double ?? 0
let coordinates = CLLocation(latitude: m.latitude ?? 0, longitude: m.longitude ?? 0)
let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
m.id = doc.data()["id"] as? String ?? ""
m.name = doc.data()["name"] as? String ?? ""
m.birthdate = doc.data()["birthdate"] as? Date ?? Date()
m.gender = doc.data()["gender"] as? String ?? ""
m.datingPreferences = doc.data()["datingPreferences"] as? String ?? ""
m.height = doc.data()["height"] as? Int ?? 0
m.imageUrl1 = doc.data()["photo1"] as? String ?? ""
m.imageUrl2 = doc.data()["photo2"] as? String ?? ""
m.imageUrl3 = doc.data()["photo3"] as? String ?? ""
m.imageUrl4 = doc.data()["photo4"] as? String ?? ""
m.imageUrl5 = doc.data()["photo5"] as? String ?? ""
m.imageUrl6 = doc.data()["photo6"] as? String ?? ""
m.Q1day2live = doc.data()["Q1day2live"] as? String ?? ""
m.QlotteryWin = doc.data()["QlotteryWin"] as? String ?? ""
m.QmoneynotanIssue = doc.data()["QmoneynotanIssue"] as? String ?? ""
m.bucketList = doc.data()["bucketList"] as? String ?? ""
m.jokes = doc.data()["jokes"] as? String ?? ""
// We have to filter out a few false positives due to GeoHash accuracy, but
// most will match
let distance = GFUtils.distance(from: centerPoint, to: coordinates)
print("MatchName: \(m.name), distance: \(distance) \tlat: \(m.latitude), \(m.longitude)")
if distance <= radiusInKilometers {
matchingDocs.append(m)
}
} //end for loop
self.matches = matchingDocs
self.usersLoaded = true
}
// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
query
.whereField("gender", in: ["Women", "men"])
.whereField("conversations", notIn: [user.name])
//.getDocuments(completion: getDocumentsCompletion)
.addSnapshotListener(getDocumentsCompletion)
}
print("Docs: \(matchingDocs.count)")
}
If you want to add additional conditions to the query you got from GeoFire, you can do so here:
return db.collection("users")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
.whereField("gender", isEqualTo: "female")
You may need to add an index for this, so be sure to check the log output for error messages around that (and a link to the Firebase console to quickly create the index).
I am currently able to pull down all of my annotations from Firebase Firestore and display them with no problem. In my snapshot listener I would like to be able to only display annotations created within the last hour. I've included my code below that isn't working and is still returning all annotations in my Firestore.
let hourAgo = Date().addingTimeInterval(-3600)
db.collection("pins").addSnapshotListener { QuerySnapshot, Error in
guard let documents = QuerySnapshot?.documents else {
print("No documents")
return
}
let annotation = documents.map { QueryDocumentSnapshot -> Pin in
let data = QueryDocumentSnapshot.data()
let latitude = data["latitude"] as? Double ?? 0.0
let longitude = data["longitude"] as? Double ?? 0.0
let eventTitle = data["eventTitle"] as? String ?? ""
let eventSubtitle = data["eventSubtitle"] as? String ?? ""
let lastUpdated = data["lastUpdated"] as? Timestamp ?? Timestamp.init()
let pinDate = lastUpdated.dateValue()
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
if pinDate >= self.hourAgo {
self.displayPins(coordinate: coordinate, eventSubtitle: eventSubtitle, eventTitle: eventTitle)
}else{
return Pin(latitude: latitude, longitude: longitude, eventTitle: eventTitle, eventSubtitle: eventSubtitle)
}
//print(annotation)
return Pin(latitude: latitude, longitude: longitude, eventTitle: eventTitle, eventSubtitle: eventSubtitle)
}
}
}
func displayPins(coordinate: CLLocationCoordinate2D, eventSubtitle: String, eventTitle: String) {
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
annotation.title = eventTitle
annotation.subtitle = eventSubtitle
mapView.addAnnotation(annotation)
}```
I used this function for an geo query. But I don't known how to add the document from the query to an array. So I can display some Map Annotations with infos from an Firestore document. How should I change it?
func geoQuery() {
// [START fs_geo_query_hashes]
// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInKilometers: Double = 50
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
withRadius: radiusInKilometers)
let queries = queryBounds.compactMap { (any) -> Query? in
guard let bound = any as? GFGeoQueryBounds else { return nil }
return db.collection("cities")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
}
var matchingDocs = [QueryDocumentSnapshot]()
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
return
}
for document in documents {
let lat = document.data()["lat"] as? Double ?? 0
let lng = document.data()["lng"] as? Double ?? 0
let coordinates = CLLocation(latitude: lat, longitude: lng)
let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
// We have to filter out a few false positives due to GeoHash accuracy, but
// most will match
let distance = GFUtils.distance(from: centerPoint, to: coordinates)
if distance <= radiusInKilometers {
matchingDocs.append(document)
}
}
}
// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
query.getDocuments(completion: getDocumentsCompletion)
}
// [END fs_geo_query_hashes]
}
https://firebase.google.com/docs/firestore/solutions/geoqueries?hl=en#swift_2 This is the Firebase documentary.
I don't know how your documents are structured or how your map is configured to display data (annotations versus regions, for example), but the general fix for your problem is to coordinate the loop of queries in your function and give them a completion handler. And to do that, we can use a Dispatch Group. In the completion handler of this group, you have an array of document snapshots which you need to loop through to get the data (from each document), construct the Pin, and add it to the map. There are a number of other steps involved here that I can't help you with since I don't know how your documents and map are configured but this will help you. That said, you could reduce this code a bit and make it more efficient but let's just go with the Firebase sample code you're using and get it working first.
struct Pin: Identifiable {
let id = UUID().uuidString
var location: MKCoordinateRegion
var name: String
var img: String
}
func geoQuery() {
// [START fs_geo_query_hashes]
// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInKilometers: Double = 50
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
withRadius: radiusInKilometers)
let queries = queryBounds.compactMap { (Any) -> Query? in
guard let bound = Any as? GFGeoQueryBounds else { return nil }
return db.collection("cities")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
}
// Create a dispatch group outside of the query loop since each iteration of the loop
// performs an asynchronous task.
let dispatch = DispatchGroup()
var matchingDocs = [QueryDocumentSnapshot]()
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
dispatch.leave() // leave the dispatch group when we exit this completion
return
}
for document in documents {
let lat = document.data()["lat"] as? Double ?? 0
let lng = document.data()["lng"] as? Double ?? 0
let name = document.data()["names"] as? String ?? "no name"
let coordinates = CLLocation(latitude: lat, longitude: lng)
let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
// We have to filter out a few false positives due to GeoHash accuracy, but
// most will match
let distance = GFUtils.distance(from: centerPoint, to: coordinates)
if distance <= radiusInKilometers {
matchingDocs.append(document)
}
}
dispatch.leave() // leave the dispatch group when we exit this completion
}
// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
dispatch.enter() // enter the dispatch group on each iteration
query.getDocuments(completion: getDocumentsCompletion)
}
// [END fs_geo_query_hashes]
// This is the completion handler of the dispatch group. When all of the leave()
// calls equal the number of enter() calls, this notify function is called.
dispatch.notify(queue: .main) {
for doc in matchingDocs {
let lat = doc.data()["lat"] as? Double ?? 0
let lng = doc.data()["lng"] as? Double ?? 0
let name = doc.data()["names"] as? String ?? "no name"
let coordinates = CLLocation(latitude: lat, longitude: lng)
let region = MKCoordinateRegion(center: <#T##CLLocationCoordinate2D#>, latitudinalMeters: <#T##CLLocationDistance#>, longitudinalMeters: <#T##CLLocationDistance#>)
let pin = Pin(location: region, name: name, img: "someImg")
// Add pin to array and then to map or just add pin directly to map here.
}
}
}
Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, annotationItems: pvm.allPins) { pin in
MapAnnotation(coordinate: pin.location.coordinate) {
Image(pin.img)
}
}
Below I have a list of cities and I compare a user's current location to the cities and use the (min) function
let closestCity = min(theDistanceInMetersFromBusselton,theDistanceInMetersFromBunbury,theDistanceInMetersFromJoondalup,theDistanceInMetersFromArmadale)
to return the closest city, though now I would like to return the second and third closest city.
I haven't been able to get this to work as yet though I'm thinking something along the lines of:
citiesArray - closestCity = SecondClosestCitiesArray
then do a secondClosestCity = min(SecondClosestCitiesArray) to get the second closest city.
Then repeat this to find the third closest?
Any ideas?
extension HomePage {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
let db = Firestore.firestore()
guard let uid = Auth.auth().currentUser?.uid else { return }
let location = locations[0]
let actualLatitude = String(location.coordinate.latitude)
let actualLongitude = String(location.coordinate.longitude)
guard let doubleActualLatitude = Double(actualLatitude) else { return }
guard let doubleActualLongitude = Double(actualLongitude) else { return }
///users current location
let usersCurrentLocation = CLLocation(latitude: doubleActualLatitude, longitude: doubleActualLongitude)
//////////////list of locations ////////////////
//////////ARMADALE/////////////////////
let ArmadaleLatitude = Double(-32.1530)
let ArmadaleLongitude = Double(116.0150)
let ArmadaleCoordinates = CLLocation(latitude:ArmadaleLatitude, longitude: ArmadaleLongitude)
let theDistanceInMetersFromArmadale = usersCurrentLocation.distance(from: ArmadaleCoordinates)
////////////////Bunbury///////////////////
let BunburyLatitude = Double(-33.3256)
let BunburyLongitude = Double(115.6396)
let BunburyCoordinates = CLLocation(latitude:BunburyLatitude, longitude: BunburyLongitude)
let theDistanceInMetersFromBunbury = usersCurrentLocation.distance(from: BunburyCoordinates)
/////////////////////////////////////////////
////////Busselton//////////////////
let busseltonLatitude = Double(-33.6555)
let busseltonLongitude = Double(115.3500)
let busseltonCoordinates = CLLocation(latitude:busseltonLatitude, longitude: busseltonLongitude)
let theDistanceInMetersFromBusselton = usersCurrentLocation.distance(from: busseltonCoordinates)
/////////////////////////////////
/////////Joondalup////////////////////
let JoondalupLatitude = Double(-32.5361)
let JoondalupLongitude = Double(115.7424)
let JoondalupCoordinates = CLLocation(latitude:JoondalupLatitude, longitude: JoondalupLongitude)
let theDistanceInMetersFromJoondalup = usersCurrentLocation.distance(from: JoondalupCoordinates)
//////////////////////////////////////
/////return the the closest city
let closestCity = min(theDistanceInMetersFromBusselton,theDistanceInMetersFromBunbury,theDistanceInMetersFromJoondalup,theDistanceInMetersFromArmadale)
func findClosestCity(){
//////////Armadale////////////////////////
if closestCity == theDistanceInMetersFromArmadale{
db.collection("Users").document(uid).setData(["Location": "Armadale" ], options: SetOptions.merge())
/////////Bunbury////////////
}else if closestCity == theDistanceInMetersFromBunbury{
let Bunbury = "Bunbury"
db.collection("Users").document(uid).setData(["Location": Bunbury ], options: SetOptions.merge())
///////////// Busselton//////////////
}else if closestCity == theDistanceInMetersFromBusselton{
let Busselton = "Busselton"
db.collection("Users").document(uid).setData(["Location": Busselton ], options: SetOptions.merge())
/////////////Joondalup//////////////////
}else if closestCity == theDistanceInMetersFromJoondalup{
db.collection("Users").document(uid).setData(["Location": "Joondalup" ], options: SetOptions.merge())
}
}
}
}
let cityLocations = [
"Armadale": CLLocation(latitude: -32.1530, longitude: 116.0150),
"Bunbury": CLLocation(latitude: -33.3256, longitude: 115.6396),
"Busselton": CLLocation(latitude: -33.6555, longitude: 115.3500),
"Joondalup": CLLocation(latitude: -32.5361, longitude: 115.7424)
]
func distanceFromCity(_ city: String, location: CLLocation) -> Double? {
return cityLocations[city].flatMap { location.distance(from: $0) }
}
func citiesClosestToLocation(_ location: CLLocation, n: Int) -> [String] {
let cities = cityLocations.sorted {
location.distance(from: $0.value) < location.distance(from: $1.value)
}
return cities.dropLast(cities.count - n).map({ $0.key })
}
let testLocation = cityLocations["Armadale"]!
print(citiesClosestToLocation(testLocation, n: 3)) // ["Armadale", "Joondalup", "Bunbury"]
You can add these value into an array and while adding it, make sure that they are getting added to sorting order. The array at any given point will give you closest city on the first index and second closest city on the second index.
Here is an example :
extension Array where Element == Double {
mutating func appendSorted(_ element: Double) {
if self.count == 0 {
self.append(element)
return
}
for i in 0..<self.count {
if element < self[i] {
self.insert(element, at: i)
return
}
}
self.append(element)
}
}