first I want to say I'm new to the swift language.
My question almost mirrors this question: Accessing MKLocalSearchResponse item (swift)
However, when I apply this to my similar looking code I get an error "Value of type 'MKLocalSearch' has no member 'mapItems'"
Like in the link above I want the first mapItems (mapItems[0]) result.
Can anybody help me?
Heres my code:
let latitude = String(currentLocation.coordinate.latitude)
let longitude = String(currentLocation.coordinate.longitude)
var station1Unwrapped: String! = ""
var station2Unwrapped: String! = ""
var coord: CLLocationCoordinate2D!
coord = CLLocationCoordinate2DMake(currentLocation.coordinate.latitude, currentLocation.coordinate.longitude);
var region: MKCoordinateRegion!
region = MKCoordinateRegion(center: coord, latitudinalMeters: 100, longitudinalMeters: 100);
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "Train Station"
request.region = region
let search = MKLocalSearch(request: request)
search.start { response, error in
guard let response = response else {
print("There was an error searching for: \(String(describing: request.naturalLanguageQuery)) error: \(String(describing: error))")
return
}
print("Inside function")
let station1 = response.mapItems[0].name
}
var newLocVar = (search.mapItems[0] as! MKMapItem).name
print(newLocVar)
The variable search is MKLocalSearch, so it doesn't has property mapItems. If you want to print the MKMapItem's name, you should access the mapItems in the completion block, where you get access to the response which is MKLocalSearch.Response. The line you write let station1 = response.mapItems[0].name is perfectly correct and it contains the name of the first mapItems found
Related
I am trying to log touch coordinates into the CSV file, but I am getting "No exact matches in call to initializer" error. I need to parse the touch coordinates as String. What is going wrong here?
super.touchesBegan(touches , with: event)
let touch = touches.first!
let location = touch.location(in: view) //Returns the current location of the receiver in the coordinate system.
let mylocation = String(location) ***No exact matches in call to initializer
// print("touches began \(location)")
let fileName2 = "testing2.csv" // CSV filename
let documentDirectoryPath2 = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let documentURL2 = URL(fileURLWithPath: documentDirectoryPath2).appendingPathComponent(fileName2)
let output2 = OutputStream.toMemory() // stream data into memory
let csvWriter2 = CHCSVWriter(outputStream: output2,
encoding: String.Encoding.utf8.rawValue, delimiter: unichar(",".utf8.first!))
csvWriter2?.writeField("Timestamp")
csvWriter2?.writeField("Timestamp in miliseconds")
csvWriter2?.finishLine()
//Array to add data
var arrOfTimestamp = [[String]]()
// var arrOfTimeInMiliSec = [[Int]]()
arrOfTimestamp.append([mylocation])
// arrOfTimeInMiliSec.append([time])
for(elements) in arrOfTimestamp.enumerated() {
csvWriter2?.writeField(elements.element[0])
}
csvWriter2?.closeStream()
let buffer = (output2.property(forKey: .dataWrittenToMemoryStreamKey) as? Data)!
do{
try buffer.write(to: documentURL2)
}
catch {
}
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)
}
}
I tried to store the city found by the reverseGeocodeLocation method inside a variable but it seems that it's not possible with the following code to reuse it inside a variable decelerated before. Debugger showed me that a city is found and is stored inside returnCity but only during execution of the reverseGeocodeLocation method.
I found some other posts but none of them helped me with my problem.
Thank you for any help!
// Get City out of coordinates
var returnCity = ""
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: self.selectedLat, longitude: self.selectedLng)
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
// Place details
var placeMark: CLPlacemark!
placeMark = placemarks?[0]
// City
if let foundCity = placeMark.locality {
returnCity = foundCity
}
})
NSLog("Sub Locality is: " + returnCity) // console shows "Sub Locality is: "
You need to use property observer to update your UI or use the updated result. I would modify "returnCity" property like below and use the data as soon as it is set and every time it is changed inside a didSet observer.
var returnCity:String? {
didSet {
guard let returnCity = self.returnCity else {return}
NSLog("Sub Locality is: " + returnCity)
}
}
I have an issue I'm trying to figure out. I'm not sure what an "elegant" way to approach this would be. This is my first Swift project, so excuse me if I'm asking something ridiculous.
I have this controller/image picker to get me the latitude/longitude of a photo in a user's photo library.
//Get metadata from a photo. Save location (latitude/longitude)
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
let picker = UIImagePickerController()
picker.delegate = self // delegate added
if let URL = info[UIImagePickerControllerReferenceURL] as? URL {
print("We got the URL as \(URL)")
let opts = PHFetchOptions()
opts.fetchLimit = 1
let assets = PHAsset.fetchAssets(withALAssetURLs: [URL], options: opts)
for assetIndex in 0..<assets.count {
var asset = assets[assetIndex]
var location = String(describing: asset.location!)
var photo_latitude = asset.location?.coordinate.latitude
var photo_longitude = asset.location?.coordinate.longitude
var coords : [String: Double] = ["longitude": photo_longitude!, "latitude": photo_latitude!]
PHAsset.fetchAssets(withALAssetURLs: [URL], options: nil)
dismiss(animated:true, completion: nil)
PHImageManager.default()
.requestImageData(for: asset,
options: nil) { resultData, response, orientation, info in
var data = resultData
DropboxClientsManager
.authorizedClient?
.files
.upload(path: "/development/image.jpg", mode: .overwrite, input : data!)
print(data!)
}
}
}
}
}
I'm also uploading the image they select to Dropbox.
Here's my issue. I want to get the address from the latitude and longitude, but I need some way to pass the latitude and longitude into the appropriate function. I'm using the following code with the Google Maps API to find an address from latitude and longitude.
let baseUrl = "http://maps.googleapis.com/maps/api/geocode/json?latlng=40.714224,-73.961452&sensor=true/false"
let apikey = "hiding_my_api_key"
func getAddressFromGoogle() {
Alamofire.request(baseUrl, method: .get).responseJSON {
response in
if response.result.isSuccess {
print("Successful request.")
var locationData = JSON(response.result.value!)
let streetAddress = locationData["results", 0, "formatted_address"]
print(streetAddress)
print(locationData)
}
else {
print("Error \(response.result.error)")
}
}
}
What would be the best way for me to pass photo_longitude and photo_latitude to my getAddressFromGoogle function?
Thanks in advance.
The typical Swift way to approach this would be to attack it head on. Latitude and longitude are both floating point numbers, and you would usually pass them as such:
func getAddressFrom(lat: Double, long: Double)
Once inside the getAddressFrom(_:_:) method, you would typically use an instance of number formatter to convert each Double to a String of the format Google’s API expects:
func getAddressFrom(lat: Double, long: Double) {
let baseUrl = "http://maps.googleapis.com/maps/api/geocode/json?", urlSuffix = "&sensor=true/false"
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = 6
// Do other configuration to get the number formatter to produce the output you need…
let numberLat = NSNumber(floatLiteral: lat), numberLong = NSNumber(floatLiteral: long)
guard let textLat = numberFormatter.string(from: numberLat),
let textLong = numberFormatter.string(from: numberLong) else {
// Handle invalid latitude or longitude…
return
}
let completeUrl = baseUrl + textLat + "," + textLong + urlSuffix
// Remaining body of function…
}
In production, you would probably configure the number formatter in the class body to reuse for many images.
Looking at your image picker controller, I see the latitude and longitude you are pulling off the images are optional values, since they may or may not be nil. When calling getAddressFrom(_:_:), you would probably use optional binding to unwrap the optionals and handle the nil case:
guard let photo_latitude = photo_latitude, let photo_longitude = photo_longitude else {
// Handle a photo missing latitude, longitude, or both…
return
}
getAddressFrom(lat: photo_latitude, long: photo_longitude)
Note that it is uncommon to use underscores in variable names in Swift. photoLatitude and photoLongitude are more Swift-y.
I have a class called User, which has a function that gets all nearby food trucks using GeoFire. I've used an observeReadyWithBlock to take the truck IDs returned by GeoFire, and get the rest of their information using Firebase. However, when I go to access one of the trucks from my array of Truck objects after adding their name and description, it looks like xCode is telling me the array is empty.
I am planning on using this array of nearby trucks in other controller classes, to populate tables showing all of the nearby trucks and some basic information to the user.
How can I properly populate my array of Trucks, and what could I be getting wrong based on the code below. Thanks very much!
func getNearbyTrucks(){
//Query GeoFire for nearby users
//Set up query parameters
let center = CLLocation(latitude: 37.331469, longitude: -122.029825)
let circleQuery = geoFire.queryAtLocation(center, withRadius: 100)
circleQuery.observeEventType(GFEventTypeKeyEntered, withBlock: { (key: String!, location: CLLocation!) in
let newTruck = Truck()
newTruck.id = key
newTruck.currentLocation = location
self.nearbyTrucks.append(newTruck)
}) //End truckQuery
//Execute code once GeoFire is done with its' query!
circleQuery.observeReadyWithBlock({
for truck in self.nearbyTrucks{
ref.childByAppendingPath("users/\(truck.id)").observeEventType(.Value, withBlock: { snapshot in
print(snapshot.value["name"] as! String)
truck.name = snapshot.value["name"] as! String
truck.description = snapshot.value["selfDescription"] as! String
let base64String = snapshot.value["profileImage"] as! String
let decodedData = NSData(base64EncodedString: base64String as String, options: NSDataBase64DecodingOptions.IgnoreUnknownCharacters)
truck.photo = UIImage(data: decodedData!)!
})
}
}) //End observeReadyWithBlock
print(nearbyTrucks[0].id)
//This line gives the error that the array index is out of range
}
The data from Geofire and the rest of your Firebase Database is not simply "gotten" from the database. It is asynchronously loaded and then continuously synchronized. This changes the flow of your code. This is easiest to see by adding some logging:
func getNearbyTrucks(){
//Query GeoFire for nearby users
//Set up query parameters
let center = CLLocation(latitude: 37.331469, longitude: -122.029825)
let circleQuery = geoFire.queryAtLocation(center, withRadius: 100)
print("Before Geoquery")
circleQuery.observeEventType(GFEventTypeKeyEntered, withBlock: { (key: String!, location: CLLocation!) in
print("In KeyEntered block ")
let newTruck = Truck()
newTruck.id = key
newTruck.currentLocation = location
self.nearbyTrucks.append(newTruck)
}) //End truckQuery
print("After Geoquery")
}
The output of the logging will be in a different order from what you may expect:
Before Geoquery
After Geoquery
In KeyEntered block
In KeyEntered block
...
While the Geo-keys and users are being retrieved from the server, the code continues and getNearbyTrucks() exits before any keys or users are returned.
One common way to deal with this is to change the way you think of your code from "first load the trucks, then print the firs truck" to "whenever the trucks are loaded, print the first one".
In code this translates to:
func getNearbyTrucks(){
//Query GeoFire for nearby users
//Set up query parameters
let center = CLLocation(latitude: 37.331469, longitude: -122.029825)
let circleQuery = geoFire.queryAtLocation(center, withRadius: 100)
circleQuery.observeEventType(GFEventTypeKeyEntered, withBlock: { (key: String!, location: CLLocation!) in
let newTruck = Truck()
newTruck.id = key
newTruck.currentLocation = location
self.nearbyTrucks.append(newTruck)
print(nearbyTrucks[0].id)
}) //End truckQuery
//Execute code once GeoFire is done with its' query!
circleQuery.observeReadyWithBlock({
for truck in self.nearbyTrucks{
ref.childByAppendingPath("users/\(truck.id)").observeEventType(.Value, withBlock: { snapshot in
print(snapshot.value["name"] as! String)
truck.name = snapshot.value["name"] as! String
truck.description = snapshot.value["selfDescription"] as! String
let base64String = snapshot.value["profileImage"] as! String
let decodedData = NSData(base64EncodedString: base64String as String, options: NSDataBase64DecodingOptions.IgnoreUnknownCharacters)
truck.photo = UIImage(data: decodedData!)!
})
}
}) //End observeReadyWithBlock
}
I've moved the printing of the first truck into the block for the key entered event. Depending on the actual code you're trying to run, you'll move it into different places.
A more reusable approach is the one the Firebase Database and Geofire themselves use: you pass a block into observeEventType withBlock: and that block contains the code to be run when a key is available. If you apply the same pattern to you method, it'd become:
func getNearbyTrucks(withBlock: (key: String) -> ()){
//Query GeoFire for nearby users
//Set up query parameters
let center = CLLocation(latitude: 37.331469, longitude: -122.029825)
let circleQuery = geoFire.queryAtLocation(center, withRadius: 100)
circleQuery.observeEventType(GFEventTypeKeyEntered, withBlock: { (key: String!, location: CLLocation!) in
let newTruck = Truck()
newTruck.id = key
newTruck.currentLocation = location
self.nearbyTrucks.append(newTruck)
withBlock(nearbyTrucks[0].id)
}) //End truckQuery
//Execute code once GeoFire is done with its' query!
circleQuery.observeReadyWithBlock({
for truck in self.nearbyTrucks{
ref.childByAppendingPath("users/\(truck.id)").observeEventType(.Value, withBlock: { snapshot in
print(snapshot.value["name"] as! String)
truck.name = snapshot.value["name"] as! String
truck.description = snapshot.value["selfDescription"] as! String
let base64String = snapshot.value["profileImage"] as! String
let decodedData = NSData(base64EncodedString: base64String as String, options: NSDataBase64DecodingOptions.IgnoreUnknownCharacters)
truck.photo = UIImage(data: decodedData!)!
})
}
}) //End observeReadyWithBlock
}
Here again, you'll want to move the withBlock() callback to a more suitable place depending on your needs.