So I want to collect user location when a user tap's a button, and use that location later on in my app. With my current implementation, when the button is tapped, the user should be prompted to allow location to be shared and then they should be logged in to the app (anonymously via Firebase in case you are wondering), and the users location information should print to the console. The prompt works, however after the allow location button is hit, my application terminates due to uncaught exception 'NSInternalInconsistencyException'
and the reason is that the 'Delegate must respond to locationManager:didUpdateLocations:'
Which is strange because I do have a didUpdateLocations: function in my code.
I have no idea what is going on, and any help is appreciated. My ViewController code is below, thanks!
/*
* Copyright (c) 2016 Ahad Sheriff
*
*/
import UIKit
import Firebase
import CoreLocation
class LoginViewController: UIViewController {
// MARK: Properties
var ref: FIRDatabaseReference! // 1
var userID: String = ""
var locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().reference() // 2
}
#IBAction func loginDidTouch(sender: AnyObject) {
//ask user to enable location services
locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
//collect user's location
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
locationManager.requestLocation()
locationManager.startUpdatingLocation()
let location = self.locationManager.location
var latitude: Double = location!.coordinate.latitude
var longitude: Double = location!.coordinate.longitude
print("current latitude :: \(latitude)")
print("current longitude :: \(longitude)")
//Log in to application anonymously using Firebase
FIRAuth.auth()?.signInAnonymouslyWithCompletion() { (user, error) in
if let user = user {
print("User is signed in with uid: ", user.uid)
self.userID = user.uid
} else {
print("No user is signed in.")
}
self.performSegueWithIdentifier("LoginToChat", sender: nil)
}
}
else {
print("Unable to determine location")
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
super.prepareForSegue(segue, sender: sender)
let navVc = segue.destinationViewController as! UINavigationController // 1
let chatVc = navVc.viewControllers.first as! ChatViewController // 2
chatVc.senderId = userID // 3
chatVc.senderDisplayName = "" // 4
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!)
{
//--- CLGeocode to get address of current location ---//
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] as CLPlacemark
self.displayLocationInfo(pm)
}
else
{
print("Problem with the data received from geocoder")
}
})
}
func displayLocationInfo(placemark: CLPlacemark?)
{
if let containsPlacemark = placemark
{
//stop updating location
locationManager.stopUpdatingLocation()
let locality = (containsPlacemark.locality != nil) ? containsPlacemark.locality : ""
let postalCode = (containsPlacemark.postalCode != nil) ? containsPlacemark.postalCode : ""
let administrativeArea = (containsPlacemark.administrativeArea != nil) ? containsPlacemark.administrativeArea : ""
let country = (containsPlacemark.country != nil) ? containsPlacemark.country : ""
print(locality)
print(postalCode)
print(administrativeArea)
print(country)
}
}
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
print("Error while updating location " + error.localizedDescription)
}
}
First, add explicitly that you confirm CLLocationManagerDelegate:
class LoginViewController: UIViewController, CLLocationManagerDelegate
Second, set up delegate property for CLLocationManager:
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
ref = FIRDatabase.database().reference()
}
Third, in CLLocationManagerDelegate doc I see different from your declaration of didUpdateLocations and didFailWithError method:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
func locationManager(_ manager: CLLocationManager, didFailWithError error: NSError)
Also, you have few other issues in code. They are all about unsafe unwrapping. You should not use unsafe unwrapping ! everywhere. Doing this you are killing optionals philosophy. Doing right thing, on other hand, can drastically increase stability of your app. In loginDidTouch function make following corrections:
var latitude: Double? = location?.coordinate.latitude
var longitude: Double? = location?.coordinate.longitude
When you call this function first time, your location not determined yet (it will be determined asynchronously, you will get location in delegate method), thats why you have fatal error when used force unwrapp. In didUpdateLocations function:
if let pm = placemarks?.first
{
self.displayLocationInfo(pm)
}
correction of this part of code is shorter then your original code, and prevents you from two possible crashes at one time - when placemarks is nil and when placemarks is not nil, but empty array
Related
So basically when the app loads it's supposed to center the map on the user location, but sometimes will get stuck at the guard let. Here is the code:
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
locationManager.delegate = self
configureLocationServices()
addDoubleTap()
}
//Center map around user location
func centerMapOnUserLocation(){
print("In \(#function)")
guard let coordinate = locationManager.location?.coordinate else { print("Error getting coordinate"); return}
let coordinateRegion = MKCoordinateRegion.init(center: coordinate, latitudinalMeters: locationZoomRadius, longitudinalMeters: locationZoomRadius)
mapView.setRegion(coordinateRegion, animated: true)
//Setting local latitude and longitude variables to current location
latitude = "\(coordinate.latitude)"
longitude = "\(coordinate.longitude)"
previewDataOnButton()
print("Centered Map")
}
// MARK: - Location Services
//Request to enable location services
func configureLocationServices(){
if authorizationStatus == .notDetermined{
locationManager.requestAlwaysAuthorization()
} else {
return
}
}
//If authorization changes then call centerMapOnUserLocation
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
print("Authorization changed in \(#function). Calling centerMapOnUserLocation")
centerMapOnUserLocation()
}
}
It seems to get stuck at the guard let in centerMapOnUserLocation(). This is shown through print statements:
Authorization changed in locationManager(_:didChangeAuthorization:). Calling
centerMapOnUserLocation
In centerMapOnUserLocation()
Error getting coordinate
Im not sure how to fix this. Like I said sometimes it passes by the guard let and sometimes it gets stuck.
In you viewDidLoad add this line:
locationManager.startUpdatingLocation()
Then use this CLLocationManagerDelegate method:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
centerMapOnUserLocation()
}
You could pass in the locations into your method and move guard let before passing the location like this:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
centerMapOnUserLocation(location: location.last)
}
This is working:
geoFire.setLocation(CLLocation(latitude: 37.7853889, longitude: -122.4056973),
forKey: "firebase-hq") { (error) in
if (error != nil) {
println("An error occured: \(error)")
} else {
println("Saved location successfully!")
}
}
But how do I send my current location coordinate to my Firebase? Like this? but currentLocation doesn't work.
geoFire!.setLocation(currentLocation, forKey: "firebase-hq") { (error) in
if (error != nil) {
print("An error occured: \(error)")
} else {
print("Saved location successfully!")
}
Unless it is completely necessary I would recommend not using GeoFire and instead use apple's inbuilt API. First, open your info.plist file as source code and add the following to the file
<key>NSLocationAlwaysUsageDescription</key>
<string>Will you allow this app to always know your location?
</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Do you allow this app to know your current location?</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Will you allow this app to always know your current location?
</string>
Then add this code to your new viewcontroller:
import UIKit
import CoreLocation
class SearchViewController: UIViewController, CLLocationManagerDelegate {
var locationManager:CLLocationManager!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
determineMyCurrentLocation()
}
func determineMyCurrentLocation() {
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.startUpdatingLocation()
//locationManager.startUpdatingHeading()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let userLocation:CLLocation = locations[0] as CLLocation
// Call stopUpdatingLocation() to stop listening for location updates,
// other wise this function will be called every time when user location changes.
// manager.stopUpdatingLocation()
print("user latitude = \(userLocation.coordinate.latitude)")
print("user longitude = \(userLocation.coordinate.longitude)")
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error)
{
print("Error \(error)")
}
}
This will print the your latitude and longitude as the default current location in your simulator as somewhere in San Francisco. To change this, go into you simulator and go into the debug menu and click location, current location and enter your preferred new values.
I am trying to use CLLocation to capture longitude and latitude and then use the longitude and latitude in Alamofire to get weather. Every time, the longitude and latitude won't stop updating and the weather data won't print(if you wanna check it out here's an example link of the data: http://forecast.weather.gov/MapClick.php?lat=37.33233141&lon=-122.0312186&FcstType=json)
class SampleViewController: UIViewController, CLLocationManagerDelegate {
var locationManager:CLLocationManager!
var startLocation: CLLocation!
var isFetchingWeather = false
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
}
override func viewDidAppear(_ animated: Bool) {
getCurrentLocation()
}
func getCurrentLocation(){
if CLLocationManager.locationServicesEnabled(){
locationManager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
var userLocation:CLLocation = locations[0]
if isFetchingWeather != false{
print("user latitude = \(userLocation.coordinate.latitude)")
print("user longitude = \(userLocation.coordinate.longitude)")
let requestLink = "http://forecast.weather.gov/MapClick.php?lat=\(userLocation.coordinate.latitude)&lon=\(userLocation.coordinate.longitude)&FcstType=json"
print(requestLink)
Alamofire.request(requestLink).validate().responseJSON
{ response in
switch response.result {
case .success(let data):
let json = JSON(data)
self.weatherData = json["data"].arrayValue
for weather in self.weatherData{
let temp = weather["weather"].stringValue
self.weatherString.append(temp)
}
print (self.weatherString)
if self.startLocation == nil {
self.startLocation = userLocation as! CLLocation
self.locationManager.stopUpdatingLocation()
}
case .failure(let error):
print(error)
}
}
}
else{
print("is fetching weather is false")
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error)
{
print("Error \(error)")
}
}
Thanks.
You really shouldn't run your weather request inside your location delegate. Instead, get your location in the didUpdateLocations delegate and save it to a var. Next, call stopUpdatingLocation() then call a separate function to make your weather request. Something like this:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let newLocation = locations.last
//check accuracy and timestamp of location to make sure its not a cached/old location (if you don't care about accuracy or time, you can remove this check)
let timeDiff = newLocation?.timestamp.timeIntervalSinceNow
if timeDiff < 5.0 && (newLocation?.horizontalAccuracy)!<=self.accuracyNeeded{
//stop updating location
self.locationManager.stopUpdatingLocation()
//set currentUserLocation
self.myLocation=newLocation?.coordinate
//call function to get weather
//remove delegate
self.locationManager.delegate = nil
}
}
Set a flag to indicate when you start fetching weather info and do not call Alamofire to fetch weather information if that flag is set. For example, you would declare something like after the line where you declare startLocation:
var isFetchingWeather = false
Then, in locationManagerdidUpdateLocations first check if isFetchingWeather is false. If not, return. Otherwise, fetch the weather info.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if isFetchingWeather {
return
}
isFetchingWeather = true
// Do the actual weather fetching
}
Of course, you might want to do the actual fetching of the weather after you've gotten a few location updates since the initial ones might not be that accurate :)
In my console, I can access Firebase latitude, longitude, and name of restaurant. I have a few print statements that I use to test that I am getting the values I needed. When I try to assign these values into an annotation.coordinate = CLLocationCoordinate2DMake(with the respective info here) I still can't get this function to print in maps.
I mainly created a function so that I can call it in viewDidLoad() so that everything I want automatically comes up when this page of the app loads.
I also created an action for a button so that when a user clicks a button on shown on the view controller, it also prints the location but THAT case also is not working. When I make coordinates I also do not know if I need ! on res.latitude and res.longitude!…. When I take it off it still does not work. A previous project I created I added annotations the same way I did here MINUS the firebase array. I created one myself with struct variables (title, latitude, longitude) in them that are later called.
import UIKit
import Firebase
import FirebaseStorage
import FirebaseDatabase
import MapKit
import CoreLocation
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {//add last 2 delegates/protocols.conforming
//firebase refrences
var dataBaseRef: FIRDatabaseReference! {
return FIRDatabase.database().reference()
}
var storageRef: FIRStorageReference! {
return FIRStorage.storage().reference()
}
var restaurantArray = [Restaurant]()
#IBOutlet weak var mapView: MKMapView!
#IBOutlet weak var segments: UISegmentedControl!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
title = "Maps"
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Main Menu", style: .plain, target: self, action: #selector(SSASideMenu.presentLeftMenuViewController))
self.locationManager.delegate = self//as soon as loaded find location--conforms to delegate
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest//best location
self.locationManager.requestWhenInUseAuthorization()//only want location when using app
self.locationManager.startUpdatingLocation()//turn on location manager..make location start looking
self.mapView.showsUserLocation = true//shows blue dot
displayRestaurants()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
fetchRestaurants()
displayRestaurants()
}
//see if you want to give it a try!!!!!!!!!!!!!!!
func fetchRestaurants(){
FIRDatabase.database().reference().child("AthensRestaurants/Restaurants").observe(.value, with: { (snapshot) in
var results = [Restaurant]()
for res in snapshot.children{
let res = Restaurant(snapshot: res as! FIRDataSnapshot)
results.append(res)
}
self.restaurantArray = results
}) { (error) in
print("error encountered dumbass")
print(error.localizedDescription)
}
}
//work here!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
func displayRestaurants(){
//array already created--using a firebase array
for res in restaurantArray {
let annotation = MKPointAnnotation()
annotation.title = res.name
print(res.name)
let x = res.latitude //shows that this works and I can retrieve data!!!
print (x! as Double)
let y = res.longitude
print(y! as Double)
annotation.coordinate = CLLocationCoordinate2D(latitude: res.latitude!, longitude: res.longitude!) //Should the exclamation marks be there "!"
mapView.addAnnotation(annotation)
}
}
//A way of testing
#IBAction func test12(sender: UIButton) {
displayRestaurants() //another way i tried makiing anotations show....!!!!!!!!!
}
//below works on seperate projects
//MARK: - Location Delegate Methods
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {//didupdate is contiously called so below is continuously called
let location = locations[0]
let span = MKCoordinateSpanMake(0.01, 0.01)
let myLocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(location.coordinate.latitude, location.coordinate.longitude)
let region = MKCoordinateRegionMake(myLocation, span)//lat long--region that we want map to scope to--parameters is closeness zoom
self.mapView.setRegion(region, animated: true)//since we have thise we can stop updating eventually
self.locationManager.stopUpdatingLocation()
}
//check for errors
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {//should be NSError but
print("Errors:" + error.localizedDescription)
//displayRestaurants()
}
//segment changer for terrain, hybrid, and regular---just allows different types of map views
#IBAction func segChange(_ sender: Any) {
switch segments.selectedSegmentIndex {
case 0:
mapView.mapType = MKMapType.standard
break
case 1:
mapView.mapType = MKMapType.satellite
break
case 2:
mapView.mapType = MKMapType.hybridFlyover
break
default:
break
}
}
}
What if the location Privacy Access for an app is Denied?
Evening guys, I'm coding a simple App that uses location when in use.
The Design Pattern
Before everything, when you launch the App, it will check if there is a permission already set.
If not, it shows an alert asking for permission.
If yes and granted, it proceeds doing its job.
If yes and denied, it shows an alert asking to grant access with a button pointing to settings.
it should be possible to return back from the settings to the app.
The apps does its job.
If the user change the privacy settings when the app is still open, the app should be notified, and repeat number 1.
The Code so Far
MainController
let manager = LocationManager()
override func viewDidLoad() {
super.viewDidLoad()
// ...
if manager.getPermission() == false {
//show alert
showAcessDeniedAlert()
}
manager.onLocationFix = {
//This is a function used for a closure
}
}
}
func showAcessDeniedAlert() {
let alertController = UIAlertController(title: "Location Accees Requested",
message: "The location permission was not authorized. Please enable it in Settings to continue.",
preferredStyle: .alert)
let settingsAction = UIAlertAction(title: "Settings", style: .default) { (alertAction) in
// THIS IS WHERE THE MAGIC HAPPENS!!!!
if let appSettings = URL(string: UIApplicationOpenSettingsURLString) {
UIApplication.shared.open(appSettings as URL)
}
}
alertController.addAction(settingsAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
LocationManager
import CoreLocation
extension Coordinate {
init(location: CLLocation) {
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
}
}
final class LocationManager: NSObject, CLLocationManagerDelegate {
let manager = CLLocationManager()
var onLocationFix: ((Coordinate) -> Void)?
override init() {
super.init()
manager.delegate = self
manager.requestLocation()
}
func getPermission() -> Bool {
switch CLLocationManager.authorizationStatus() {
case .authorizedAlways:
return true
case .authorizedWhenInUse:
return true
case .denied:
return false
case .restricted:
return false
case .notDetermined:
manager.requestWhenInUseAuthorization()
return getPermission()
}
}
//MARK: CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
let coordinate = Coordinate(location: location)
if let onLocationFix = onLocationFix {
onLocationFix(coordinate)
}
}
}
How Can I?
How can I show The AlertController if the privacy is Denied?
With this setup I'm having this error: Warning: Attempt to present <UIAlertController: 0x145ae7ee0> on <xxx.xxController: 0x143e04720> whose view is not in the window hierarchy!.
How can I code the setting button pointing to Settings?
How can I code: "from settings page, I could return to the app"?
viewDidLoad is called after first access self.view property. This mean that for first is called viewDidLoad after this self.view is added on window hierarchy. Move checking code to viewDidAppear(_:) function and be sure that your view controller is presented.
Code that open Settings app seems to be ok, but don't forget to check from which system version it's available.
Your app is not able to interact somehow with other apps while it's in background state.