So I've got the following function to set my map region for my app:
func mapRegion() -> MKCoordinateRegion {
databaseHandle = databaseRef.child("RunList").child(runName).observe(.value, with: { (snapshot) in
let runData = snapshot.value as? [String: AnyObject]
self.minLat = runData?["startLat"] as? Double
self.minLng = runData?["startLong"] as? Double
self.maxLat = runData?["endLat"] as? Double
self.maxLng = runData?["endLong"] as? Double
print("testing")
print(self.minLat!)
print(self.maxLng!)
self.coordinate = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: (self.minLat! + self.maxLat!)/2,
longitude: (self.minLng! + self.maxLng!)/2),
span: MKCoordinateSpan(latitudeDelta: (self.maxLat! - self.minLat!)*1.1,
longitudeDelta: (self.maxLng! - self.minLng!)*1.1))
})
return coordinate
}
I have initialised the following variables at the top of my class:
var minLat: Double!
var minLng: Double!
var maxLat: Double!
var maxLng: Double!
var coordinate: MKCoordinateRegion!
And am trying to set the map region as follows:
override func viewDidLoad() {
super.viewDidLoad()
configureView()
mapView.region = mapRegion()
}
The error I am getting is in the lines where I am setting my coordinate region under the block of code that contains my databaseHandle in mapRegion(). When the code is running, I get the following error under the line 'return coordinate':
fatal error: unexpectedly found nil while unwrapping an Optional value
To my understanding, this is occurring because 'coordinate' has no values and is an Optional. Why is it that coordinate has no values even though I am setting it up using a global variable? I feel like I am missing something so simple!
Thanks
Your code does not work because observe works asynchronously and it is impossible to return a value from a method containing a asynchronous task.
You need a completion handler
func mapRegion(completion: (MKCoordinateRegion)->()) {
databaseHandle = databaseRef.child("RunList").child(runName).observe(.value, with: { (snapshot) in
let runData = snapshot.value as? [String: AnyObject]
self.minLat = runData?["startLat"] as? Double
self.minLng = runData?["startLong"] as? Double
self.maxLat = runData?["endLat"] as? Double
self.maxLng = runData?["endLong"] as? Double
print("testing")
print(self.minLat!)
print(self.maxLng!)
let region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: (self.minLat! + self.maxLat!)/2,
longitude: (self.minLng! + self.maxLng!)/2),
span: MKCoordinateSpan(latitudeDelta: (self.maxLat! - self.minLat!)*1.1,
longitudeDelta: (self.maxLng! - self.minLng!)*1.1))
completion(region)
})
}
And use it
mapRegion() { region in
mapView.region = region
// do other things with the region
}
Side note:
There are too many question marks in the code. Use optional bindings to unwrap optionals safely.
Related
I come to you because I have the following problem:
I work with the 'GoogleMaps' cocoapods and I need to place several markers in a map by using the latitude, longitude and a codeID that I get from an API. I will present you guys 2 cases: the one that works (that uses 3 hard coded arrays mentioned before) and the one that I try to get from the API and that crashes no matter what I do. OK, the first case (the one that works) is this one:
import UIKit
import GoogleMaps
class ViewController: UIViewController {
// MARK: - Constants and variables
let lat: Double = 38.739429 // User's Latitude
let lon: Double = -9.137115 // User's Longitude
let zoom: Float = 15.0
// MARK: - Elements in the storyboard
#IBOutlet weak var googleMap: GMSMapView!
// MARK: - ViewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
googleMapsStuff()
}
// MARK: - Google maps method
func googleMapsStuff() {
googleMap.delegate = self
self.googleMap.isMyLocationEnabled = true // User's current position (blue dot on the map)
let arrayLat: [Double] = [38.739, 38.74, 38.741, 38.732, 38.7325, 38.733]
let arrayLon: [Double] = [-9.136, -9.135, -9.134, -9.137, -9.1375, -9.138]
//var arrayCompanyZoneID: [Int] = []
let camera: GMSCameraPosition = GMSCameraPosition.camera(withLatitude: lat, longitude: lon, zoom: self.zoom)
googleMap.camera = camera
for index in 0..<arrayLon.count {
let marker = GMSMarker()
marker.position = CLLocationCoordinate2D(latitude: arrayLat[index], longitude: arrayLon[index])
marker.title = "Marker number: \(index)"
marker.snippet = "Marker's Lat: \(arrayLat[index]), Marker's Lon: \(arrayLon[index])"
marker.map = self.googleMap
print("Index: \(index)")
}
}
}
And as you can see in the image, it all goes smoothly well
The problem comes in the second case, when I try to fill the empty arrays (which it seems to do) when I connect to an API to get that data. This is the "failure" case:
struct MyInfo: Codable {
let id: String
let name: String
let x: Double // Longitude
let y: Double // Latitude
let licencePlate: String?
let range: Int?
let batteryLevel: Int?
let seats: Int?
let model: String?
let resourceImageId: String?
let pricePerMinuteParking: Int?
let pricePerMinuteDriving: Int?
let realTimeData: Bool?
let engineType: String?
let resourceType: String?
let companyZoneId: Int
let helmets: Int?
let station: Bool?
let availableResources: Int?
let spacesAvailable: Int?
let allowDropoff: Bool?
let bikesAvailable: Int?
}
class ViewController: UIViewController {
// MARK: - Constants and variables
let lat: Double = 38.739429 // User's Latitude
let lon: Double = -9.137115 // User's Longitude
let zoom: Float = 15.0
var arrayLat: [Double] = [] // [38.7395, 38.739, 38.74, 38.741, 38.732, 38.7325, 38.733]
var arrayLon: [Double] = [] // [-9.1365, -9.136, -9.135, -9.134, -9.137, -9.1375, -9.138]
var arrayCompanyZoneID: [Int] = [] // [1, 2, 3, 4, 5, 6, 7]
// MARK: - Elements in the storyboard
#IBOutlet weak var googleMap: GMSMapView!
// MARK: - ViewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
googleMap.delegate = self
self.googleMap.isMyLocationEnabled = true // User's current position (blue dot on the map)
let camera: GMSCameraPosition = GMSCameraPosition.camera(withLatitude: self.lat, longitude: self.lon, zoom: self.zoom)
googleMap.camera = camera
guard let urlAPI = URL(string: "https://apidev.meep.me/tripplan/api/v1/routers/lisboa/resources?lowerLeftLatLon=38.711046,-9.160096&upperRightLatLon=38.739429,-9.137115") else { return }
let task = URLSession.shared.dataTask(with: urlAPI) {(data, response, error) in
if error == nil {
guard let urlContent = data else { return }
do {
let JSONResult = try JSONDecoder().decode([MyInfo].self, from: urlContent) //JSONSerialization.jsonObject(with: urlContent, options: .mutableContainers)
print("JSON Result:", JSONResult)
for jsonData in JSONResult {
self.arrayLon.append(jsonData.x)
self.arrayLat.append(jsonData.y)
self.arrayCompanyZoneID.append(jsonData.companyZoneId)
}
print("-----------------")
print(type(of: JSONResult))
print("-----------------")
print("ArrayLon:", self.arrayLon)
print("ArrayLat:", self.arrayLat)
print("companyZoneId: ", self.arrayCompanyZoneID)
print("Count zoneid: ", self.arrayCompanyZoneID.count)
print("-----------------")
// MARK: - Place the multiple markers on the map
for index in 0..<self.arrayCompanyZoneID.count {
let marker = GMSMarker()
marker.position = CLLocationCoordinate2D(latitude: self.arrayLat[index], longitude: self.arrayLon[index])
marker.title = "Marker number: \(index)"
marker.snippet = "Marker's Lat: \(self.arrayLat[index]), Marker's Lon: \(self.arrayLon[index])"
marker.map = self.googleMap
print("Index: \(index)")
}
} catch {
print("JSON processing failed.")
}
} else {
print("Error serializing JSON:", error!)
}
}
task.resume()
}
And it doesn't matter what I do, the console always says:
"Terminating app due to uncaught exception 'GMSThreadException', reason: 'The API method must be called from the main thread' "
I also tried using the method
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
but it also says that the API method most be called from the main thread.
I'm so stuck here and I invested several hours to this issue but It only fails over and over.
I appreciate your advice and wisdom.
Thanks in advance.
You need
DispatchQueue.main.async {
// MARK: - Place the multiple markers on the map
for index in 0..<self.arrayCompanyZoneID.count {
let marker = GMSMarker()
marker.position = CLLocationCoordinate2D(latitude: self.arrayLat[index], longitude: self.arrayLon[index])
marker.title = "Marker number: \(index)"
marker.snippet = "Marker's Lat: \(self.arrayLat[index]), Marker's Lon: \(self.arrayLon[index])"
marker.map = self.googleMap
print("Index: \(index)")
}
}
As URLSession.shared.dataTask callback is in a background thread
I try to display the distance between a post creator and the current user within a cell.
My error now is
Cannot convert value of type 'String?' to expected argument type 'CLLocationDegrees' (aka 'Double')
in the following code:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let lastLocation = locations.last {
let geoCoder = CLGeocoder()
let myLocation = CLLocation(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)
/*Here the error*/ let myBuddysLocation = CLLocation(latitude: job!.distance, longitude: job!.distance)
let distance = myLocation.distance(from: myBuddysLocation) / 1000
print(String(format: "The distance to my buddy is %.01fkm", distance))
}
}
This is my JobClass:
class Job {
var distance: String?
let ref: DatabaseReference!
init(distance: String? = nil) {
self.distance = distance
ref = Database.database().reference().child("jobs").childByAutoId()
}
init(snapshot: DataSnapshot){
ref = snapshot.ref
if let value = snapshot.value as? [String : Any] {
distance = value["distance"] as? String
}
}
func save() {
let newPostKey = ref.key
let postDictionary = [
"distance" : self.distance!
] as [String : Any]
self.ref.setValue(postDictionary)
}
}
I hope someone knows how to solve it, if you wan't I can add more code if it helps // I'm new to coding
The error is self-explanatory: You are trying to set a String? instead of a CLLocationDegrees, but you can even use a Double type.
There are several ways to fix this:
1) You can change your class definition as it follows:
class Job {
// Change distance to Double and set a default value instead of optional
var distance: Double = 0.0
let ref: DatabaseReference!
// Change the init
init(distance: Double = 0.0) {
self.distance = distance
ref = Database.database().reference().child("jobs").childByAutoId()
}
//You have to change all your distance to Double
init(snapshot: DataSnapshot){
ref = snapshot.ref
if let value = snapshot.value as? [String : Any] {
// So change this cast
distance = value["distance"] as Double
}
}
}
2) You can try a cast from String? to Double, but I don't recommend this.
Anyway, here it is how you can do it:
if let dist = Double(job!.distance){
let myBuddysLocation = CLLocation(latitude: dist, longitude: dist) -> Error Appears in this Line
let distance = myLocation.distance(from: myBuddysLocation) / 1000
print(String(format: "The distance to my buddy is %.01fkm", distance))
}
Actually in your function the only problem are there you passing the string value in place of double you need to change like this.
let myBuddysLocation = CLLocation(latitude: Double.init(job!.distance) ?? 0.0, longitude: Double.init(job!.distance) ?? 0.0)
You need to change your function like this
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let lastLocation = locations.last {
let geoCoder = CLGeocoder()
let myLocation = CLLocation(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)
let myBuddysLocation = CLLocation(latitude: Double.init(job!.distance) ?? 0.0, longitude: Double.init(job!.distance) ?? 0.0)
let distance = myLocation.distance(from: myBuddysLocation) / 1000
print(String(format: "The distance to my buddy is %.01fkm", distance))
}
}
It's normal because distance should be Double value. Changing it to: var distance: Double? should correct it.
Or parse the value to Double let myBuddysLocation = CLLocation(latitude: Double(distance)!, longitude: Double(distance)!)
I recommend you to avoid using ! because it could generate a fatal error if the string isn't a number, you can guard the value with guard or if let like this example:
if let latitude = Double(distance) {
let myBuddysLocation = CLLocation(latitude: latitude, longitude: latitude)
}
I am encountering great difficulties in putting an integrated map based on firebase in my project, I am looking for expert knowledge to help me, even though I build run correctly at the time of running the system for, my code below:
Thanks
#IBOutlet var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
let locationsRef = Database.database().reference(withPath: "locations")
locationsRef.observe(.value, with: { snapshot in
for item in snapshot.children {
guard let locationData = item as? DataSnapshot else { continue }
var locationValue = locationData.value as! [String: Any]
var location: CLLocationCoordinate2D!
if let lat = locationValue["lat"] as? String {
let lng = Double(locationValue["lng"] as! String)!
location = CLLocationCoordinate2D(latitude: Double(lat)!, longitude: lng)
} else {
let lat = locationValue["lat"] as! Double
let lng = locationValue["lng"] as! Double
location = CLLocationCoordinate2D(latitude: lat, longitude: lng)
}
func addAnnotations(coords: [CLLocation]){
for coord in coords{
let CLLCoordType = CLLocationCoordinate2D(latitude: coord.coordinate.latitude,
longitude: coord.coordinate.longitude);
let anno = MKPointAnnotation();
anno.coordinate = CLLCoordType;
self.mapView.addAnnotation(anno);
}
}
}
})
}
So I had issues previously working with 'observe' from firebase, and I realised I could not bring the variable values from inside the code block that was working asynchronously. A user told me to use completion handlers to resolve this issue, and his example was:
func mapRegion(completion: (MKCoordinateRegion)->()) {
databaseHandle = databaseRef.child("RunList").child(runName).observe(.value, with: { (snapshot) in
let runData = snapshot.value as? [String: AnyObject]
self.minLat = runData?["startLat"] as? Double
self.minLng = runData?["startLong"] as? Double
self.maxLat = runData?["endLat"] as? Double
self.maxLng = runData?["endLong"] as? Double
print("testing")
print(self.minLat!)
print(self.maxLng!)
let region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: (self.minLat! + self.maxLat!)/2,
longitude: (self.minLng! + self.maxLng!)/2),
span: MKCoordinateSpan(latitudeDelta: (self.maxLat! - self.minLat!)*1.1,
longitudeDelta: (self.maxLng! - self.minLng!)*1.1))
completion(region)
})
}
and to use the code:
mapRegion() { region in
mapView.region = region
// do other things with the region
}
So I've tried to recreate this for another method that I need to return an array of object type RunDetail:
func loadRuns(completion: ([RunDetail]) -> ()) {
// we need name, distance, time and user
databaseHandle = databaseRef.child("RunList").observe(.value, with: { (snapshot) in
self.count = Int(snapshot.childrenCount)
print(self.count!)
// more stuff happening here to add data into an object called RunDetail from firebase
// add RunDetail objects into array called 'run'
})
completion(runs)
}
I am not sure if I am setting this up correctly above^.
I still cannot get my head around getting the completion handler working (I really don't understand how to set it up). Can someone please help me and let me know if I am setting this up properly? Thanks.
You need to move the completion(region) to inside the Firebase completion block and add #escaping after completion:.
Also, you should not force unwrap optionals. It is easy enough to check that they are not nil and this will prevent the app from crashing.
func mapRegion(completion: #escaping (MKCoordinateRegion?) -> Void) {
let ref = Database.database().reference()
ref.child("RunList").child(runName).observe(.value, with: { (snapshot) in
guard
let runData = snapshot.value as? Dictionary<String,Double>,
let minLat = runData["startLat"],
let minLng = runData["startLong"],
let maxLat = runData["endLat"],
let maxLng = runData["endLong"]
else {
print("Error! - Incomplete Data")
completion(nil)
return
}
var region = MKCoordinateRegion()
region.center = CLLocationCoordinate2D(latitude: (minLat + maxLat) / 2, longitude: (minLng + maxLng) / 2)
region.span = MKCoordinateSpanMake((maxLat - minLat) * 1.1, (maxLng - minLng) * 1.1)
completion(region)
})
}
Then update your code to this.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
mapRegion { (region) in
if let region = region {
self.mapView.setRegion(region, animated: true)
}
}
}
For your loadRuns
func loadRuns(completion: #escaping (Array<RunDetail>) -> Void) {
let ref = Database.database().reference()
ref.child("RunList").observe(.value, with: { (snapshot) in
var runs = Array<RunDetail>()
// Populate runs array.
completion(runs) // This line needs to be inside this closure.
})
}
I am trying to convert a String that I am retrieving from Firebase and adding it as several annotations on Google Maps. Unfortuanately, my app is crashing whenever it goes through the current code:
ref = FIRDatabase.database().reference()
ref.child("Locations").observeSingleEventOfType(.Value, withBlock: { (snapshot) in
let lat = (snapshot.value!["Latitude"] as! NSString).doubleValue
let lon = (snapshot.value!["Longitude"] as! NSString).doubleValue
let complainLoc = CLLocationCoordinate2DMake(lat, lon)
let Coordinates = CLLocationCoordinate2D(latitude: lat, longitude: lon)
})
My JSON Tree
My Code Block Which Crashes
Here is the code I used for saving data to Firebase
FIRDatabase.database().reference().child("Location").child(FIRAuth.auth()!.currentUser!.uid).setValue(["Latitude": locationManager.location!.coordinate.latitude, "Longitude": locationManager.location!.coordinate.longitude])
SWIFT 5
let dbLat = Double(latStr) // Convert String to double
let dbLong = Double(longStr)
Use latitude and longitude
let center = CLLocationCoordinate2D(latitude: dbLat! , longitude: dbLong! )
let pointAnnotation = MKPointAnnotation()
pointAnnotation.coordinate = CLLocationCoordinate2D(latitude: dbLat!, longitude:dbLong!)
Make sure when you are saving the values of lat and lon to the database you are saving them as Float or Double..
For retrieving use :
ref = FIRDatabase.database().reference()
ref.child("Locations").observeEventType(.Value, withBlock: { (snapshot) in
if snapshot.exists(){
if let locationDictionary = snapshot.value as [String : AnyObject]{
for each in locationDictionary{
//each will bring you every location dictionary in your database for your every user
let lat = each.value!["Latitude"] as! CLLocationDegrees
let lon = each.value!["Longitude"] as! CLLocationDegrees
let userId = each.key as! String
let complainLoc = CLLocationCoordinate2DMake(lat, lon)
let Coordinates = CLLocationCoordinate2D(latitude: lat, longitude: lon)
//Every time this for loop complete's itself it will generate a new set of Coordinates for each user
}
}
}
})
EDIT:
Updated code for Firebase 6 and Swift 5
let ref = self.ref.child("Locations")
ref.observeSingleEvent(of: .value, with: { snapshot in
let allLocations = snapshot.children.allObjects as! [DataSnapshot]
for location in allLocations {
let lat = location.childSnapshot(forPath: "Latitude").value as! CLLocationDegrees
let lon = location.childSnapshot(forPath: "Longitude").value as! CLLocationDegrees
let userId = location.key
let locCoord = CLLocationCoordinate2DMake(lat, lon)
let coordinates = CLLocationCoordinate2D(latitude: lat, longitude: lon)
}
})
note that self.ref points to my Firebase root ref.