Swift 3.0 Get User Location Coordinates from Different View Controller - ios

I have two view controllers - one with the mapView that is able to obtain user location coordinations through locationManager, and a second VC that I wish to be able to pull these user coordinates.
First VC: MapView
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
var coordinatesOfUser = locations.last?.coordinate
print("The value of usercoordinates are \(coordinatesOfUser)")
// here I want to be able to pull this variable, coordinatesOfUser
if let location = locations.last {
let span = MKCoordinateSpanMake(0.00775, 0.00775)
let myLocation = CLLocationCoordinate2DMake(location.coordinate.latitude,location.coordinate.longitude)
let region = MKCoordinateRegionMake(myLocation, span)
map.setRegion(region, animated: true)
}
self.map.showsUserLocation = true
locationManager.stopUpdatingLocation()
}
Second VC:
I was thinking of calling the locationManager function in this VC. Is this the most efficient way to pull the coordinates to this VC? And if so, how would I go about doing it?

Here's a couple options to solve this:
Delegation: Your secondVC could have a delegate that allows the first view controller to get coordinates from it. The advantage here is that you could receive updates as the come in.
protocol MyLocationDelegate: class {
func newLocationArrived(location: CLLocation)
}
class FirstVC: UIViewController, MyLocationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
}
func newLocationArrived(location: CLLocation) {
print(location)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let dest = segue.destination as? SecondVC {
dest.delegate = self
}
}
}
class SecondVC: UIViewController, CLLocationManagerDelegate {
/// ... ...
weak var delegate: MyLocationDelegate?
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
/// do something with the location
/// provide the data via delegation
delegate?.newLocationArrived(location: CLLocation())
}
}
Notifications: Post a notification via NSNotificationCenter. Also able to receive updates as the come in, just send via notification center instead of through a delegate.
postNotificationName:object:userInfo:
Child View Controller: Depending on whether the second view controller and its view are a child of the first, this could allow direct access. Not always an option.
Singleton (CLLocationManager): If you plan to use Location Services in other places throughout the app, you can move the CLLocationManager into its own class with a Singleton. Other view controllers can reference that class for their specific needs. This can also be helpful when using the background or significant change locations as they might need to use the LaunchOptions Key to restart the location manager.
class MyLocationManager: CLLocationManager, CLLocationManagerDelegate {
static let shared = MyLocationManager()
var locations = [CLLocation]()
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
for location in locations {
self.locations.append(location)
}
}
}

I had the same problem and I finally fixed it with a Notification, as kuhncj said.
This is how the code looks like at the end:
//Get user location
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
mapView.isMyLocationEnabled = true
mapView.settings.myLocationButton = true
} else {
initialLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
mapView.camera = GMSCameraPosition(target: location.coordinate, zoom: 15, bearing: 0, viewingAngle: 0)
locationManager.stopUpdatingLocation()
let userInfo: NSDictionary = ["location": location]
//Post notification
NotificationCenter.default.post(name: NSNotification.Name("UserLocationNotification"), object: self, userInfo: userInfo as [NSObject : AnyObject])
}
}
And then in the other view controller:
var userLocation: String?
override func viewDidLoad() {
super.viewDidLoad()
//Observer that receives the notification
NotificationCenter.default.addObserver(self, selector: #selector(locationUpdateNotification), name: Notification.Name("UserLocationNotification"), object: nil)
}
func locationUpdateNotification(notification: NSNotification) {
if let userInfo = notification.userInfo?["location"] as? CLLocation {
//Store user location
self.userLocation = "\(userInfo.coordinate.latitude), \(userInfo.coordinate.longitude)"
}
}
Then I was able to use the stored userLocation for another methods.

Related

Open SwiftUI view only if location permission granted

I have a view (say V) in which a user answers a few questions and their location is recorded. However, the answers only make sense with the user's location.
So what I want is that when the user clicks on a button on the parent view, it takes them to V and immediately asks them for the location permission. If they accept, they can continue on to answer the questions, but if they deny, they navigate back to the parent screen.
I know I can navigate back to the parent screen with self.presentation.wrappedValue.dismiss().
But how do I know when the user has accepted or denied the permission since requestWhenInUseAuthorization() is an asynchronous function?
I'm following this tutorial on getting a user's location on iOS with Swift.
Code for my LocationService:
import CoreLocation
protocol LocationServiceDelegate {
func didFetchCurrentLocation(_ location: GeoLocation)
func fetchCurrentLocationFailed(error: Error)
}
class LocationService: NSObject, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
var delegate: LocationServiceDelegate
init(delegate: LocationServiceDelegate) {
self.delegate = delegate
super.init()
self.setupLocationManager()
}
private func setupLocationManager() {
if canUseLocationManager() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
}
func requestLocation() {
if canUseLocationManager() {
print(CLAuthorizationStatus.self)
locationManager.requestWhenInUseAuthorization()
locationManager.requestLocation()
}
}
func requestPermission() {
locationManager.requestWhenInUseAuthorization()
}
private func canUseLocationManager() -> Bool {
return CLLocationManager.locationServicesEnabled()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print(locations)
if let location = locations.last {
let geoLocation = GeoLocation(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
delegate.didFetchCurrentLocation(geoLocation)
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
delegate.fetchCurrentLocationFailed(error: error)
}
deinit {
locationManager.stopUpdatingLocation()
}
}
struct GeoLocation {
var latitude: Double
var longitude: Double
}
CLLocationManagerDelegate has also the following method:
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
}
This method is called every time the authorization status changed. I would also like to recommend you implementing your LocationService as an ObservableObject instead of using delegate approach.

Double segue behavior when user authorizes location use

In my iOS application I’m requesting the user’s permission to obtain the device’s location. On my main ViewController I have a navigation bar button that when tapped, it will ask the user for permission for when in use. If the user taps OK, it will then be send to the view controller that displays the local data. If the user taps Cancel then nothing happens. I also have a pop up for when and if the user taps again on the location button to be redirected to the settings to authorize location use if previously cancelled.
The app works as I intended in the simulator but when used on a device, when the user taps OK to allow location use, it segues to the local View Controller but it does so 2 or 3 times consecutively.
The segue goes from the main view controller to the local view controller and it requests permissions from the button tap using an IBAction.
The location information is obtained in the main view controller and passed to the local controller. The local controller displays everything as it is intended.
How can I prevent this double or triple segue to the same View Controller?
Below is my code:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "toLocal" {
let destination = segue.destination as! LocalViewController
destination.latitude = latitude
destination.longitude = longitude
}
}
//MARK: - Location Manager Methods
#IBAction func LocationNavBarItemWasTapped(sender: AnyObject) {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations[locations.count - 1]
if location.horizontalAccuracy > 0 {
locationManager.stopUpdatingLocation()
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
}
let status = CLLocationManager.authorizationStatus()
switch status {
case .restricted, .denied:
showLocationDisabledPopUp()
return
case .notDetermined:
// Request Access
locationManager.requestWhenInUseAuthorization()
case .authorizedAlways:
print("Do Nothing: authorizedAlways")
case .authorizedWhenInUse:
self.performSegue(withIdentifier: "toLocal", sender: nil)
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("LocationManager failed with error \(error)")
}
private func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if (status == CLAuthorizationStatus.denied) {
showLocationDisabledPopUp()
}
}
As superpuccio already stated the main issue is that the didUpdateLocations delegate function is called multiple times. I also do not know why you are checking the authorizationStatus in the didUpdateLocations function since at that point it is already clear that the user allowed location access. In my opinion the function should look something like this:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// check for a location that suits your needs
guard let location = locations.last, location.horizontalAccuracy > 0 else { return }
// prevent the manager from updating the location and sending more location events
manager.delegate = nil
manager.stopUpdatingLocation()
// update the local variables
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
// perform the segue
performSegue(withIdentifier: "toLocal", sender: nil)
}
Since there are some more issues like starting location updates before knowing the actual authorization status I'll provide a full solution like I'd do it. Feel free to ask if anything is unclear:
class ViewController: UIViewController {
var latitude: CLLocationDegrees?
var longitude: CLLocationDegrees?
lazy var locationManager: CLLocationManager = {
let locationManager = CLLocationManager()
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
return locationManager
}()
#IBAction func getLocation(_ sender: UIBarButtonItem) {
locationManager.delegate = self
checkAuthorizationStatus()
}
private func checkAuthorizationStatus(_ status: CLAuthorizationStatus? = nil) {
switch status ?? CLLocationManager.authorizationStatus() {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .authorizedWhenInUse:
locationManager.startUpdatingLocation()
default:
showLocationDisabledPopUp()
}
}
func showLocationDisabledPopUp() {
// your popup code
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// your segue code
}
}
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
checkAuthorizationStatus(status)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last, location.horizontalAccuracy > 0 else { return }
manager.delegate = nil
manager.stopUpdatingLocation()
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
performSegue(withIdentifier: "toLocal", sender: nil)
}
}
The issue here is that you are performing a segue in
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
that can be fired multiple times (each time a new location comes from the CLLocationManager). There are several ways to solve this, but in order to make you change as little as possible I suggest this:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations[locations.count - 1]
if location.horizontalAccuracy > 0 {
locationManager.stopUpdatingLocation()
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
}
let status = CLLocationManager.authorizationStatus()
switch status {
case .restricted, .denied:
showLocationDisabledPopUp()
return
case .notDetermined:
// Request Access
locationManager.requestWhenInUseAuthorization()
case .authorizedAlways:
print("Do Nothing: authorizedAlways")
case .authorizedWhenInUse:
//-- MODIFIED HERE --
locationManager.stopUpdatingLocation()
locationManager = nil
Dispatch.main.async {
self.performSegue(withIdentifier: "toLocal", sender: nil)
}
}
}
Let me know if it helps, otherwise we can change a little more your code to solve this simple issue.
EDIT: just so you know: it would be better to perform the segue in
private func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus)
when the status is kCLAuthorizationStatusAuthorized or kCLAuthorizationStatusAuthorizedAlways or kCLAuthorizationStatusAuthorizedWhenInUse depending on your needs.

how to convey the coordinate from didUpdateLocation to some view controller?

I am trying to make LocationService class that will convey userCoordinate, so it will be reusable for more than one View Controller
import UIKit
import CoreLocation
class LocationService: NSObject {
let manager = CLLocationManager()
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.startUpdatingLocation()
}
func getPermission() {
// to ask permission to the user by showing an alert (the alert message is available on info.plist)
if CLLocationManager.authorizationStatus() == .notDetermined {
manager.requestWhenInUseAuthorization()
}
}
func checkLocationAuthorizationStatus() -> CLAuthorizationStatus{
return CLLocationManager.authorizationStatus()
}
}
extension LocationService : CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("location is not available: \(error.localizedDescription)")
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let userLocation = locations.first else {return}
if userLocation.horizontalAccuracy > 0 {
manager.stopUpdatingLocation()
let coordinate = Coordinate(location: userLocation)
// how to convey this coordinate for several view controller?
}
}
}
as you can see in the didUpdateLocations method that comes from CLLocationManagerDelegate, the coordinate need some time to be generated.
but I don't know how to convey that user coordinate, I think it will use completion handler but I don't know how to get that
so let say in HomeVC, I will call that LocationService to get the userCoordinate
import UIKit
class HomeVC: UIViewController {
let locationService = LocationService()
override func viewDidLoad() {
super.viewDidLoad()
// get coordinate, something like this
locationService.getCoordinate()
}
}
You can use Notification like Paulw11 said. You need to update didUpdateLocations. This is the place where you are going to post notification.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let userLocation = locations.first else {return}
if userLocation.horizontalAccuracy > 0 {
manager.stopUpdatingLocation()
let coordinate = Coordinate(location: userLocation)
let locationDictionary: [String: Double] = ["lat": location.coordinate.latitude,
"long": location.coordinate.longitude]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "YourNotificationNameAsYouWantToNameIt"), object: nil, userInfo: locationDictionary)
}
}
Now in viewDidLoad in every view controller that you want this location you need to observe this notification:
NotificationCenter.default.addObserver(self, selector: #selector(doSomethingAboutLocation(_:)), name: NSNotification.Name(rawValue: "YourNotificationNameAsYouWantToNameIt"), object: nil)
Then access your location like this in your function called from selector:
#objc func doSomethingAboutLocation(_ notification: Notification) {
if let notificationInfo = notification.userInfo {
let coordinate = CLLocation(latitude: notificationInfo["lat"] as! Double, longitude: notificationInfo["long"] as! Double)
// use your coordinate as you want
}
}

Swift 3 - store the User Location and call it from different View Controllers

I'm pretty new in programming and this is my first app, so sorry if the approach is very shabby.
I created a helper method to get the user location, because I need to call it from different view controllers so I thought this was a cleaner way to do it. But I don't know why is not working now (no errors, it just show the general view of Europe). But when it was inside the view controller it worked perfectly fine.
I got this new approach from the course I'm doing and I've been researching in many sources. I've also checked this question but I didn't find any solution yet.
Here is the method I created in the GMSClient file. It will get the user location, but if the user disables this option, it will show the default position (centred in Berlin):
extension GMSClient: CLLocationManagerDelegate {
//MARK: Initial Location: Berlin
func setDefaultInitialLocation(_ map: GMSMapView) {
let camera = GMSCameraPosition.camera(withLatitude: 52.520736, longitude: 13.409423, zoom: 8)
map.camera = camera
let initialLocation = CLLocationCoordinate2DMake(52.520736, 13.409423)
let marker = GMSMarker(position: initialLocation)
marker.title = "Berlin"
marker.map = map
}
//MARK: Get user location
func getUserLocation(_ map: GMSMapView,_ locationManager: CLLocationManager) {
var userLocation: String?
locationManager.requestWhenInUseAuthorization()
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
map.isMyLocationEnabled = true
map.settings.myLocationButton = true
} else {
setDefaultInitialLocation(map)
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
map.camera = GMSCameraPosition(target: location.coordinate, zoom: 15, bearing: 0, viewingAngle: 0)
locationManager.stopUpdatingLocation()
//Store User Location
userLocation = "\(location.coordinate.latitude), \(location.coordinate.longitude)"
print("userLocation is: \((userLocation) ?? "No user Location")")
}
}
}
}
This file has also this singelton:
// MARK: Shared Instance
class func sharedInstance() -> GMSClient {
struct Singleton {
static var sharedInstance = GMSClient()
}
return Singleton.sharedInstance
}
And then I call it in my view controller like this:
class MapViewController: UIViewController, CLLocationManagerDelegate {
// MARK: Outlets
#IBOutlet weak var mapView: GMSMapView!
// MARK: Properties
let locationManager = CLLocationManager()
var userLocation: String?
let locationManagerDelegate = GMSClient()
// MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
self.locationManager.delegate = locationManagerDelegate
GMSClient.sharedInstance().getUserLocation(mapView, locationManager)
}
Anyone has an idea of what can be wrong?
Thanks!
Following what Paulw11 said, I found the faster solution using Notifications.
Send notification from the LocationManager delegate method inside the first view Controller:
class MapViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
mapView.isMyLocationEnabled = true
mapView.settings.myLocationButton = true
} else {
initialLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
mapView.camera = GMSCameraPosition(target: location.coordinate, zoom: 15, bearing: 0, viewingAngle: 0)
locationManager.stopUpdatingLocation()
let userInfo : NSDictionary = ["location" : location]
NotificationCenter.default.post(name: NSNotification.Name("UserLocationNotification"), object: self, userInfo: userInfo as [NSObject : AnyObject])
}
}
}
Set the second view controller as observer. This way I can store the userLocation and use it later for the search request:
class NeighbourhoodPickerViewController: UIViewController, UITextFieldDelegate {
var userLocation: String?
var currentLocation: CLLocation!
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(locationUpdateNotification), name: Notification.Name("UserLocationNotification"), object: nil)
}
func locationUpdateNotification(notification: NSNotification) {
if let userInfo = notification.userInfo?["location"] as? CLLocation {
self.currentLocation = userInfo
self.userLocation = "\(userInfo.coordinate.latitude), \(userInfo.coordinate.longitude)"
}
}
I guess the problem is here,
self.locationManager.delegate = locationManagerDelegate
You have created a new instance of GMSClient, and saved it in the stored property and that instance is set as the delegate property of CLLocationManager.
You need to do this instead,
self.locationManager.delegate = GMSClient.sharedInstance()
You need to do this because you would want singleton instance of GMSClient to be the delegate for CLLocationManager and not a new instance. That way your singleton class would recieve the callbacks from
CLLocationManager class.
To understand more about why your code was not working, I would suggest you read more about Objects, Instances, Instance variables, Singletons, Delegate design pattern.

CLLocationCoordinates from locationManager didUpdateLocations

I am trying to use CoreLocation (once permission is granted) to get a user's CLLocationCoordinate2D, so that I can pass that information to an Uber deeplink (after a UIButton within my TableView is pressed).
I've figured out how to get the coordinates as CLLocations, and turn them into CLLocationCoordinates2D in the didUpdateLocations method, but can't seem to transfer them over to my buttonPressed function.
Can anyone explain how I can properly transfer the coordinates info to the uberButtonPressed method? I am also confused about how to get the locationManager to stop updating location once a suitable location is determined. Any help is much appreciated. By the way I am using this to instantiate Uber: https://github.com/kirby/uber
import UIKit
import CoreLocation
import MapKit
class ATableViewController: UITableViewController, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
var location: CLLocation?
var coordinate: CLLocationCoordinate2D?
// Implemented tableView methods etc here...
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
let newLocation = locations.last as! CLLocation
println("DID UPDATE LOCATIONS \(newLocation)")
location = newLocation
coordinate = location!.coordinate
println("WE HAVE THE COORDINATES \(coordinate!.latitude) and \(coordinate!.longitude)") // this prints along with DID UPDATE LOCATIONS
}
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
println("error:" + error.localizedDescription)
}
func uberButtonPressed(sender: UIButton!) {
let senderButton = sender
println(senderButton.tag)
let authStatus: CLAuthorizationStatus = CLLocationManager.authorizationStatus()
if authStatus == .NotDetermined {
locationManager.requestWhenInUseAuthorization()
return
}
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
var pickupLocation = coordinate! // fatal error: unexpectedly found nil while unwrapping an Optional value
println(pickupLocation)
// // Create an Uber instance
// var uber = Uber(pickupLocation: pickupLocation)
//
// Set a few optional properties
// uber.pickupNickname = "OK"
//
// uber.dropoffLocation = CLLocationCoordinate2D(latitude: 47.591351, longitude: -122.332271)
// uber.dropoffNickname = "whatever"
//
// // Let's do it!
// uber.deepLink()
//
}
You should move var pickupLocation = coordinate! into your didUpdateLocations. Once assigned, you can call locationManager.stopUpdatingLocation() also from inside 'didUpdateLocation or your value for coordinate with keep updating. After stopping the locationManager, call a NEW function to run the rest of your code currently in func uberButtonPressed
I had the same problem.
Instead of calling the delegate method like this:
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
debugPrint("NOT CALLED-- didUpdateLocations AnyObject")
}
I changed into:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
debugPrint("CALLED-- didUpdateLocations CLLocation")
}
The problem is that you are unwrapping coordinate which is a CLLocationCoordinate2D? with a !. Since coordinate is from location which is a CLLocation?. It is possible that coordinate is nil and location is nil, especially before location services have had a chance to kick in. Only run the rest of uberButtonPressed: for the case where coordinate and location are not nil by using if let currentCoordinate = coordinate?, and in that case, call locationManager.stopUpdatingLocation().

Resources