swift: what is the correct way of working with current location - ios

I am quite new to swift and IOS development.
I would like to ask experienced members what is the correct way of creating mapvie and tableview in one viewcontroller and populating them.
The logic of application is the following:
get current location of user
read plist file with POI coordinates
for each POI run a function wich calculates distance between user and point
populate table with data from plist file plus newly calculated data.
Both mapview and tableview are in the same viewcontroller.
in viewDidLoad I am getting users location.
in viewwillappear I am running functions to read plist file and calculate distances between POIs and user.
Everything is working but it is not stable ... sometimes it might show user's location but table will be empty. So I doubt that I am doing everything correctly. Also probably it is not correct to put both map and table inside one class?
Update
Here is the code:
import UIKit
import MapKit
import CoreLocation
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var theMapView: MKMapView!
#IBOutlet weak var tableView: UITableView!
var locationManager: CLLocationManager!
var branches = [Branch]()
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
self.locationManager.requestAlwaysAuthorization()
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
tableView.dataSource = self
tableView.delegate = self
locationManager.startUpdatingLocation()
locationManager.startUpdatingHeading()
//mapview setup to show user location
theMapView.delegate = self
theMapView.showsUserLocation = true
theMapView.mapType = MKMapType(rawValue: 0)!
theMapView.userTrackingMode = MKUserTrackingMode(rawValue: 2)!
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
readFromPlist()
}
//MARK: UITableView methods
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return branches.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: MapCustomCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! MapCustomCell
let brnch = branches[indexPath.row]
cell.mapSetupCell(brnch.cityName, AddressLabel: brnch.address, DistanceLabel: brnch.distance)
return cell
}
//MARK: FUNC TO CALCULATE DISTANCE
func calculateDistance (lat1 lat1: Double, lon1: Double, lat2: Double, lon2: Double) -> String {
return "100km"
}
func locationManager (manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let myCoordinates = locations.last
let myLat = myCoordinates!.coordinate.latitude
let myLong = myCoordinates!.coordinate.longitude
let myCoordinates2D = CLLocationCoordinate2DMake(myLat, myLong)
let myLatDelta = 0.10
let myLongDelta = 0.10
let mySpan = MKCoordinateSpanMake(myLatDelta, myLongDelta)
let myRegion = MKCoordinateRegion(center: myCoordinates2D, span: mySpan)
theMapView.setRegion(myRegion, animated: true)
let myAnno = MKPointAnnotation()
myAnno.coordinate = myCoordinates2D
self.locationManager.stopUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print("error:" + error.localizedDescription)
}
func readFromPlist() {
//read plist file to extract banks coordinates
let path = NSBundle.mainBundle().pathForResource("poi", ofType: "plist")
let POIarrays = NSArray(contentsOfFile: path!)
for arr in POIarrays! {
var ctName : String!
var brnchAddress : String!
var wrkngHours : String!
var lat : Double!
var long : Double!
ctName = arr.objectForKey("cityName")! as! String
brnchAddress = arr.objectForKey("address")! as! String
wrkngHours = arr.objectForKey("workingHours")! as! String
lat = Double(arr.objectForKey("latitude")! as! String)
long = Double(arr.objectForKey("longitude")! as! String)
let latitude: CLLocationDegrees = lat
let longitude : CLLocationDegrees = long
let bankLocation : CLLocationCoordinate2D = CLLocationCoordinate2DMake(latitude, longitude)
let annotation = MKPointAnnotation()
annotation.coordinate = bankLocation
annotation.title = bnkName
annotation.subtitle = brnchAddress
self.theMapView.addAnnotation(annotation)
let myLatitude = self.locationManager.location?.coordinate.latitude
let myLongitude = self.locationManager.location?.coordinate.longitude
if myLatitude != nil {
let dist = calculateDistance(lat1: latitude, lon1: longitude, lat2: myLatitude!, lon2: myLongitude!)
let b = Branch(cityName: ctName!, address: brnchAddress!, distance: dist)
branches.append(b)
}
}
}
}
Sometimes I got an error "error:The operation couldn’t be completed. (kCLErrorDomain error 0.)" and current location doesn't appear on my map.

Related

Display the distance from user with mapKit Swift (miles/km)

I have been attempting to display the distance on a tableView but I am unable to get it to happen. This question follows up from this question: CLLocationDistance conversion. I have checked the distance. Using this function in my Location class:
// Get distance
func distance(to location: CLLocation) -> CLLocationDistance {
return location.distance(from: self.location)
}
How I get the users current location:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations[0]
let span:MKCoordinateSpan = MKCoordinateSpanMake(0.01, 0.01)
let myLocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(location.coordinate.latitude, location.coordinate.longitude)
let region:MKCoordinateRegion = MKCoordinateRegionMake(myLocation, span)
mapView.setRegion(region, animated: true)
// Add a lastUserLocation to LocationManager and update it every time that the delegate receives a new location
LocationManager.shared.lastUserLocation = locations.last
LocationManager.shared.sortLocationsInPlace()
self.mapView.showsUserLocation = true
}
Sort function in LocationManager:
func getSortedLocations(userLocation: CLLocation) -> [Location] {
return locations.sorted { (l1, l2) -> Bool in
return l1.distance(to: userLocation) < l2.distance(to: userLocation)
}
}
func sortLocationsInPlace() {
if let validLocation = lastUserLocation {
locations.sort { (l1, l2) -> Bool in
return l1.distance(to: validLocation) < l2.distance(to: validLocation)
}
}
}
cellForRowAt:
var sortedLocations = [Location]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "locationCell", for: indexPath)
let location = sortedLocations[indexPath.row]
cell.textLabel?.text = location.name
return cell
}
Update
Inside Location class:
class Location {
var name: String
var latitude: Double
var longitude: Double
var location:CLLocation {
return CLLocation(latitude: latitude, longitude: longitude)
}
init?(json: JSON) {
guard let name = json["name"] as? String, let latitude = json["latitude"] as? Double, let longitude = json["longitude"] as? Double else { return nil }
self.name = name
self.latitude = latitude
self.longitude = longitude
}
func distance(to location: CLLocation) -> CLLocationDistance {
return location.distance(from: self.location)
}
}
Considering your code, I am making some assumptions:
Your sortedLocations array has different locations that you extracted from a JSON or whatever.
You call startUpdatingLocation() or similar somewhere before loading your data.
You are receiving updates in your didUpdateLocations.
Your LocationManager keeps an ordered copy of all your locations in a variable called locations, the one you are ordering inside didUpdateLocations.
That considered, what I understand you want to do is to display your sortedLocations ordered according to a reference location.
What is missing is to update your UITableView data once your user location is received. You have two main options:
To only load your UITableView once you have already your first user location retrieved by didUpdateLocations.
To force a UITableView update once you get a new location, by calling tableView.reloadData() inside didUpdateLocations. This will redraw your list every time you receive a location update, sorting them by location.
However, in any of those cases you need to replace your cellForRow text to display your distance instead of location.name:
// Distance in meters
cell.textLabel?.text = String(location.distance(to: LocationManager.shared.lastUserLocation!))
// Distance in miles
cell.textLabel?.text = String(location.distance(to: LocationManager.shared.lastUserLocation!)*0.00062137)
And update your didUpdateLocations:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
let span:MKCoordinateSpan = MKCoordinateSpanMake(0.01, 0.01)
let myLocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(location.coordinate.latitude, location.coordinate.longitude)
let region:MKCoordinateRegion = MKCoordinateRegionMake(myLocation, span)
mapView.setRegion(region, animated: true)
// Add a lastUserLocation to LocationManager and update it every time that the delegate receives a new location
LocationManager.shared.lastUserLocation = location
LocationManager.shared.sortLocationsInPlace()
sortedLocations = LocationManager.shared.locations
tableView.reloadData()
self.mapView.showsUserLocation = true
}
}
With your current code you are comparing all distances with a self.location variable that its not being initialised anywhere apparently.

Firebase database now only displaying newly added annotations on map

I have an app that allows people to put annotations on a map that get stored in a Firebase database, I have recently been trying to add a new feature to the app so to avoid messing about with my real database I created another app in firebase and used it for me to do my testing on. I deleted the GoogleService-Info.Plist and imported the one for the new test app but now when I delete the test GoogleService-Info.Plist and replace it with the one that works with my Live app none of my annotations show up on the map, but any I create from that point onwards will show up.
I've done a clean and build but still the same result. All the data is still there in my original database as my AppStore version of the app works exactly as it should, I really don't know what could be wrong here, I'll let you see what my code is for retrieving the annotations, in case that helps you with your answer.
import UIKit
import MapKit
import Firebase
class MapViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
#IBOutlet weak var postboxesLoggedLabel: UILabel!
var locationManager = CLLocationManager()
let annotation = MKPointAnnotation()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
displayAnnotations()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let userLocation: CLLocation = locations[0]
let latitude = userLocation.coordinate.latitude
let longitude = userLocation.coordinate.longitude
let latDelta: CLLocationDegrees = 0.05
let lonDelta: CLLocationDegrees = 0.05
let span = MKCoordinateSpan(latitudeDelta: latDelta, longitudeDelta: lonDelta)
let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let region = MKCoordinateRegion(center: location, span: span)
self.mapView.setRegion(region, animated: true)
locationManager.stopUpdatingLocation()
}
func displayAnnotations() {
let ref = Database.database().reference()
ref.child("Postbox").observe(.childAdded, with: { (snapshot) in
let monToFri = (snapshot.value as AnyObject!)!["Monday to Friday Collection Time"] as! String!
let sat = (snapshot.value as AnyObject!)!["Saturday Collection Time"] as! String!
let latitude = (snapshot.value as AnyObject!)!["Latitude"] as! String!
let longitude = (snapshot.value as AnyObject!)!["Longitude"] as! String!
let lastCollection = "Mon - Fri: \(monToFri!)" + " Sat: \(sat!)"
self.annotation.coordinate = CLLocationCoordinate2D(latitude: (Double(latitude!))!, longitude: (Double(longitude!))!)
self.annotation.title = "Last Collection:"
self.annotation.subtitle = lastCollection
self.mapView.addAnnotation(self.annotation)
self.postboxesLoggedLabel.text = String(self.mapView.annotations.count)
})
}
/* func removeAnnotation(gesture: UIGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.ended {
self.mapView.removeAnnotation(annotation)
print("Annotation Removed")
}
} */
#IBAction func myLocation(_ sender: Any) {
locationManager.startUpdatingLocation()
}
#IBAction func mapType(_ sender: Any) {
switch ((sender as AnyObject).selectedSegmentIndex) {
case 0:
mapView.mapType = .standard
case 1:
mapView.mapType = .satellite
default: // or case 2
mapView.mapType = .hybrid
}
}
}

Swift - Return value from locationManager function in class extension

I'm trying to get directions from the users current location to a destination using Google Maps. I want this to be done when the showDirection button is pressed, however I can't figure how to return or pass the users location into the IBAction function from func locationManager(... didUpdateLocation) as the IBAction doesn't use parameters in which I can pass locValue to.
Here is the showDirection button function:
#IBAction func showDirection(sender: AnyObject) {
print("Running showDirection")
let instanceOne = ParseViewController() // Create ParseViewController instance to operate on
print("Created ParseView instance")
let Coord = instanceOne.returnParse()
let latitude = (Coord.lat as NSString)
let longitude = (Coord.long as NSString)
var urlString = "http://maps.google.com/maps?"
urlString += "saddr= // Users location from didUpdateLocation"
urlString += "&daddr= \(latitude as String), \(longitude as String)"
print(urlString)
if let url = NSURL(string: urlString.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!
{
UIApplication.sharedApplication().openURL(url)
}
}
and here is the locationManager function with the locValue:
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
mapView.camera = GMSCameraPosition(target: location.coordinate, zoom: 15, bearing: 0, viewingAngle: 0)
let locValue:CLLocationCoordinate2D = (manager.location?.coordinate)!
print("Coordinates = \(locValue.latitude), \(locValue.longitude)")
locationManager.stopUpdatingLocation()
}
}
Any help is greatly appreciated!
You need to create an internal variable in the class to store the location if you want to use it in another function. E.g.
class YourViewController: UIViewController ... {
var lastLocation: CLLocation? = nil
...
}
In didUpdateLocations:
if let location = locations.first {
lastLocation = location
...
}
And now you can access it in func showDirection()

Thread 1: EXC_BAD_INSTRUCTION

I am making this app where users can see their own location and other users location. I recently just got an error saying
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_1386_INVOP,subcode=0x0)
at this line:
var lat = locationManager.location?.coordinate.latitude
I have not managed to fix it.
What is causing it and how can I fix it?
For any who might would like the rest of the code:
import UIKit
import Parse
import CoreLocation
import MapKit
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
var myLocation: [CLLocation] = []
#IBOutlet weak var MapView: MKMapView!
#IBOutlet var UsernameTextField: UITextField!
#IBOutlet var PasswordTF: UITextField!
#IBOutlet var EmailTF: UITextField!
var locationManager: CLLocationManager!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let locationManager = CLLocationManager()
let lat = locationManager.location!.coordinate.latitude
let lon = locationManager.location!.coordinate.longitude
let location = CLLocationCoordinate2D(latitude: lat, longitude: lon)
let span = MKCoordinateSpanMake(0.05, 0.05)
let region = MKCoordinateRegionMake(location, span)
MapView!.setRegion(region, animated: true)
let anotation = MKPointAnnotation()
anotation.coordinate = location
anotation.title = "My tittle"
anotation.subtitle = "My Subtitle"
MapView!.addAnnotation(anotation)
print("Welcome in MapViewController")
}
}
This is what #matt is talking about:
The problem is that you are asking for the location manager's location
without checking to see whether the result is nil
Here's how you check to see if your value is nil:
option 1:
guard let lat = locationManager.location?.coordinate.latitude else {
return
}
option 2:
if let latCheck = locationManager.location?.coordinate.latitude {
// assign your lat value here
} else {
// handle the problem
}
You need to change your mindset, when you see an ! you should probably be unwrapping your optional value in one of the above two ways.
update based on comments:
You can also try creating a new variable to work with and see how it works:
guard let location = locationManager.location else {
return
}
then:
let lat = location.coordinate.latitude
let lon = location.coordinate.longitude
The problem is that you are asking for the location manager's location without checking to see whether the result is nil. Well, it is (probably because there has not been time to get the actual location yet). Therefore, when you try to get that location's latitude and longitude, lat and lon, they are nil as well. Therefore when you force-unwrap them, you crash, because you cannot unwrap nil.

Map with Photos using Parse

I'm trying to do exactly this using Parse and Swift.
import UIKit
import MapKit
import CoreLocation
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
var latitude:Double = 0.0
var longitude:Double = 0.0
#IBOutlet var map: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.setNavigationBarHidden(true , animated: true)
self.map.showsUserLocation = true
self.map.delegate = self
PFGeoPoint.geoPointForCurrentLocationInBackground { (geopoint:PFGeoPoint?, error:NSError?) -> Void in
var query = PFQuery(className: "Locations")
query.whereKey("geopoint", nearGeoPoint: geopoint!)
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if let myObjects = objects {
for object in myObjects {
var thePoint: PFGeoPoint = object["geopoint"] as! PFGeoPoint
self.latitude = thePoint.latitude
self.longitude = thePoint.longitude
NSLog(" Hej %f, %f", self.latitude, self.longitude)
var annotationCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2DMake(self.latitude, self.longitude)
var annotation = MKPointAnnotation()
annotation.coordinate = annotationCoordinate
annotation.title = object["discovery"] as! String
annotation.subtitle = object["location"] as! String
self.map.addAnnotation(annotation)
}
}
}
}
}
Big picture though, I'm trying to create a way to access pictures stored on Parse (and their locations) and display them on a map if they meet my query. I greatly appreciate any help!
if you are getting the conversion error in
var thePoint: PFGeoPoint = object["geopoint"]
change it to
var thePoint: PFGeoPoint = object["geopoint"] as! PFGeoPoint
Regarding converting Double to Int, it appears to be as you found but I don't know the rationale behind the decision by the designers of swift. I would like to understand why you need the conversion since there may be an alternate implementation

Resources