how to wait till locaiton manager delegate called in swift? - ios

i have a general function genParam() as soon as i called this method , i want to return the current userGPS location along with other parameters.but in my case genParam() returning immediately before calling the delegate method didUpdateLocations.Is there any way to wait till delegate method gets called before returning genParam().
class CommonApiParamGenerator: NSObject,CLLocationManagerDelegate {
var locationManager = CLLocationManager()
var userGPSLoc:String = ""
func genParam(locationName:String)->NSMutableDictionary{
self.getUserLocation()
let guid = NSUUID().UUIDString
let userName = SingleTon.sharedInstance.getUserName()
let gpsLoc = self.userGPSLoc
let commonParam = NSMutableDictionary(objects: [guid,userName,gpsLoc], forKeys: ["guid","userName","gpsLoc"])
return commonParam
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
let userLocation:CLLocation = locations[0] as CLLocation
self.userGPSLoc = "\(userLocation.coordinate.latitude),\(userLocation.coordinate.longitude)"
print("receivedGPS \(self.userGPSLoc)")
manager.stopUpdatingLocation()
}
func getUserLocation()
{
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
}

Firstly accroding to single responsibility principle I propose you to move you code with getting location in separate class:
class LocationManager: NSObject, CLLocationManagerDelegate {
let manager = CLLocationManager()
private var completion: ((CLLocation) -> Void)?
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
}
func getLocation(completion: (CLLocation) -> Void) {
self.completion = completion
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
let userLocation = locations[0] as CLLocation
manager.stopUpdatingLocation()
self.completion?(userLocation)
}
}
Then refactor you class:
class CommonApiParamGenerator: NSObject {
var userGPSLoc:String = ""
var locationManager = LocationManager()
func genParam(locationName:String, completion: (NSMutableDictionary) -> Void) {
let guid = NSUUID().UUIDString
let userName = SingleTon.sharedInstance.getUserName()
locationManager.getLocation { location in
let userGPSLoc = "\(location.coordinate.latitude),\(location.coordinate.longitude)"
completion(NSMutableDictionary(objects: [guid,userName,userGPSLoc], forKeys: ["guid","userName","gpsLoc"]))
}
}
}
Usage:
let generator = CommonApiParamGenerator()
generator.genParam(locationName) { params in
...
}
Main idea is using callbacks for asynchronous operations.
If you don't like callbacks, or there are a lot of nested callbacks (callback hell) you can you PromiseKit: http://promisekit.org/.
Good luck!

Related

Can't update label after getting location

I have a simple button, when I press the button, I'm making a call to another class, my Location class to get the user's current location.
After getting the location, I want to update a label text I have to show the location.
This is my location class:
class LocationManager: NSObject, CLLocationManagerDelegate {
var locationManager: CLLocationManager!
var geoCoder = CLGeocoder()
var userAddress: String?
override init() {
super.init()
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.activityType = .other
locationManager.requestWhenInUseAuthorization()
}
func getUserLocation(completion: #escaping(_ result: String) -> ()){
if CLLocationManager.locationServicesEnabled(){
locationManager.requestLocation()
}
guard let myResult = self.userAddress else { return }
completion(myResult)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]){
let userLocation: CLLocation = locations[0] as CLLocation
geoCoder.reverseGeocodeLocation(userLocation) { (placemarks, err) in
if let place = placemarks?.last{
self.userAddress = place.name!
}
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
}
}
and this is where I call the method and updating the label:
func handleEnter() {
mView.inLabel.isHidden = false
location.getUserLocation { (theAddress) in
print(theAddress)
self.mView.inLabel.text = "\(theAddress)"
}
}
My problem is that when I click my button (and firing handleEnter()), nothing happens, like it won't register the tap. only after tapping it the second time, I get the address and the labels update's.
I tried to add printing and to use breakpoint to see if the first tap registers, and it does.
I know the location may take a few seconds to return an answer with the address and I waited, but still, nothing, only after the second tap it shows.
It seems like in the first tap, It just didn't get the address yet. How can I "notify" when I got the address and just then try to update the label?
Since didUpdateLocations & reverseGeocodeLocation methods are called asynchronously, this guard may return as of nil address
guard let myResult = self.userAddress else { return }
completion(myResult)
Which won't trigger the completion needed to update the label , instead you need
var callBack:((String)->())?
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]){
let userLocation: CLLocation = locations[0] as CLLocation
geoCoder.reverseGeocodeLocation(userLocation) { (placemarks, err) in
if let place = placemarks?.last{
callBack?(place.name!)
}
}
}
Then use
location.callBack = { [weak self] str in
print(str)
DispatchQueue.main.async { // reverseGeocodeLocation callback is in a background thread
// any ui
}
}

Core Location in MVP

In my project I have a LocationService class that conforms to CLLocationManagerDelegate protocol in order to detect current user's location.
class LocationService: NSObject, CLLocationManagerDelegate {
fileprivate let locationManager = CLLocationManager()
var location: Location? // Location(lat: Double, lon: Double)
override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
}
func getCurrentLocation() -> Location? {
locationManager.startUpdatingLocation()
// how can I catch a location?
return location
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
if location.horizontalAccuracy > 0 {
locationManager.stopUpdatingLocation()
self.location = Location(lat: location.coordinate.latitude, lon: location.coordinate.latitude)
}
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
}
I want my WeatherPresenter to trigger location updates in LocationService and get the result as soon as location is found. Is there any way to do this?
class WeatherPresenter {
unowned let delegate: WeatherViewDelegate
let weatherService = WeatherService()
let locationService = LocationService()
init(with delegate: WeatherViewDelegate) {
self.delegate = delegate
}
func getWeatherForCurrentLocation() {
if let location = locationService.getCurrentLocation() {
//...
}
}
}
You can use Delegate to notify WeatherPresenter on changes from LocationService
protocol LocationServiceDelegate: class { // Delegate protocol
func didUpdateLocation()
}
class LocationService: NSObject, CLLocationManagerDelegate {
weak var delegate: LocationServiceDelegate?
fileprivate let locationManager = CLLocationManager()
var location: Location? // Location(lat: Double, lon: Double)
override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
}
func startUpdatingLocation() { // Start updating called from presenter
locationManager.startUpdatingLocation()
}
func getCurrentLocation() -> Location? {
// how can I catch a location?
return location
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
if location.horizontalAccuracy > 0 {
locationManager.stopUpdatingLocation()
self.location = Location(lat: location.coordinate.latitude, lon: location.coordinate.latitude)
self.delegate?.didUpdateLocation() // Notify delegate on change
}
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
}
class WeatherPresenter: LocationServiceDelegate {
unowned let delegate: WeatherViewDelegate
let weatherService = WeatherService()
let locationService = LocationService()
init(with delegate: WeatherViewDelegate) {
self.delegate = delegate
self.locationService.delegate = self // Set self as delegate
self.locationService.startUpdatingLocation() // Requests start updating location
}
func didUpdateLocation() { // This will be called on location change
self.getWeatherForCurrentLocation()
}
func getWeatherForCurrentLocation() {
if let location = locationService.getCurrentLocation() {
//...
}
}
}

Delegate method didUpdateLocations is called after trying to use data that has not yet been retrieved from itself - Xcode Swift

I have an NS object (called GoogleSearch) that I use to get the user's location data. These are some global variables created and the init function:
let locationManager = CLLocationManager()
var coordinates: CLLocationCoordinate2D?
override init() {
super.init()
print("1")
DispatchQueue.main.async {
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
if CLLocationManager.locationServicesEnabled() {
self.locationManager.startUpdatingLocation()
}
}
}
Next, here is my delegate method:
extension GoogleSearch: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("2")
let userLocation: CLLocation = locations[0] as CLLocation
self.coordinates = CLLocationCoordinate2D(latitude: userLocation.coordinate.latitude, longitude: userLocation.coordinate.longitude)
}
}
Next, I try to access the coordinates (set in the delegate method) in a later function:
func searchForPlaces(completion: #escaping ([place])->()) {
print(coordinates)
}
Finally, in the ViewController that I implement this NSobject, I have the following code in viewDidLoad:
self.googleSearch = GoogleSearch()
DispatchQueue.main.async {
self.googleSearch.searchForPlaces(completion: { (places) in
DispatchQueue.main.async {
self.places = places
self.placesCollectionView.reloadData()
}
})
}
The problem is that printing coordinates in the searchForPlaces functions prints nil because it is run before the delegate method is called. Is there anything I should change in my NSObject or perhaps my ViewController to ensure that I can access coordinates from searchForPlaces() ?
Thank you.
You can achieve in this way:
class GoogleSearch:NSObject {
let locationManager = CLLocationManager()
var coordinates: CLLocationCoordinate2D?
var completionHandler: ((CLLocationCoordinate2D) -> Void)?
override init() {
super.init()
print("1")
}
func searchForPlaces(completion: #escaping (CLLocationCoordinate2D) -> Void) {
self.completionHandler = completion
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
if CLLocationManager.locationServicesEnabled() {
self.locationManager.stopUpdatingLocation()
self.locationManager.startUpdatingLocation()
}
}
}
extension GoogleSearch: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("2")
let userLocation: CLLocation = locations[0] as CLLocation
self.coordinates = CLLocationCoordinate2D(latitude: userLocation.coordinate.latitude, longitude: userLocation.coordinate.longitude)
self.completionHandler?(self.coordinates!)
}
}

How to use locationManager() in multiple ViewControllers

I need to get the zipCode and the city in multiple viewControllers.
Here is how I'm currently doing it...
import CoreLocation
let locationManager = CLLocationManager()
class MyViewController: UIViewController, CLLocationManagerDelegate{
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)-> Void in
if error != nil {
//AlertView to show the ERROR message
}
if placemarks!.count > 0 {
let placemark = placemarks![0]
self.locationManager.stopUpdatingLocation()
let zipCode = placemark.postalCode ?? ""
let city:String = placemark.locality ?? ""
// Do something with zipCode
// Do something with city
}else{
print("No placemarks found.")
}
})
}
func someFunction() {
locationManager.startUpdatingLocation()
}
Everything works fine but as you can see doing it this way in multiple viewController leads to a lot of code repetition (of course, I'm not showing the whole code).
What would be the most common way to retrieve the zipCode and city from CLLocationManager() in a more practical way from multiple viewControllers?
What I'm thinking is something like...
MyLocationManager.zipCode() // returns zipCode as a string
MyLocationManager.city() // returns city as a string
The usual thing is to have just one location manager in one persistent place that you can always get to from anywhere, like the app delegate or the root view controller.
I tried to implement a singleton CLLocationManager class, I think you can modify the following class to implement some additional methods.
import Foundation
class LocationSingleton: NSObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
private var latitude = 0.0
private var longitude = 0.0
static let shared = LocationSingleton()
private override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLLocationAccuracyHundredMeters
locationManager.requestAlwaysAuthorization() // you might replace this with whenInuse
locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
}
}
private func getLatitude() -> CLLocationDegrees {
return latitude
}
private func getLongitude() -> CLLocationDegrees {
return longitude
}
private func zipCode() {
// I think you can figure way out to implemet this method
}
private func city() {
// I think you can figure way out to implemet this method
}
}

Swift + CLLocationManager: How to tell if the user is in a specific city?

I use CLLocationManager to request the user's location. However, if they are outside of New York City, I want to default to certain coordinates. Is there a way to check if they are in a certain city?
import UIKit
import CoreLocation
import GoogleMaps
private let kDefaultLatitude: Double = 40.713
private let kDefaultLongitude: Double = -74.000
private let kDefaultZoomLevel: Float = 16.0
class RootMapViewController: UIViewController {
#IBOutlet weak var mapView: GMSMapView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
fetchLocation()
}
private func fetchLocation() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
}
// MARK: CLLocationManagerDelegate
extension RootMapViewController: CLLocationManagerDelegate {
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locationManager.stopUpdatingLocation()
let userCoordinates = locations[0].coordinate
// How do I check if the user is in NYC?
// if user is in nyc
centerMapOn(userCoordinates)
mapView.myLocationEnabled = true
mapView.settings.myLocationButton = true
// else default to Times Square
}
}
You can use reverse geocoding. For example you can place:
geocoder:CLGeocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(locations[0],completionHandler{
if error == nil && placemarks.count > 0 {
let location = placemarks[0] as CLPlacemark
print(location.locality)
})
in func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation])

Resources