How do you get user location permissions in SwiftUI?
I tried asking for user location permissions after a button tap, but the dialogue box disappears after about a second. Even if you do end up clicking it in time, permission is still denied.
import CoreLocation
.
.
.
Button(action: {
let locationManager = CLLocationManager()
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
}) {
Image("button_image")
}
Things like location manager should be in your model, not your view.
You can then invoke a function on your model to request location permission.
The problem with what you are doing now is that your CLLocationManager gets released as soon as the closure is done. The permission request methods execute asynchronously so the closure ends very quickly.
When the location manager instance is released the permission dialog disappears.
A location model could look something like this:
class LocationModel: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
#Published var authorisationStatus: CLAuthorizationStatus = .notDetermined
override init() {
super.init()
self.locationManager.delegate = self
}
public func requestAuthorisation(always: Bool = false) {
if always {
self.locationManager.requestAlwaysAuthorization()
} else {
self.locationManager.requestWhenInUseAuthorization()
}
}
}
extension LocationModel: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
self.authorisationStatus = status
}
}
You would probably also want functions to start & stop location updates and an #Published CLLocation property
Related
I'm trying to execute a Location Service once. This LocationService is called from another object class, that will add the location info into the parameters. All of this will be one object.
The problem is that when I init the object, everything is populated less the location data, which will be populated a few ms later.
I need to wait until the callback is executed, to successfully generate the full object before using it
So considering that I have the next "LocationService" class
public class LocationService: NSObject, CLLocationManagerDelegate{
let manager = CLLocationManager()
var locationCallback: ((CLLocation?) -> Void)!
var locationServicesEnabled = false
var didFailWithError: Error?
public func run(callback: #escaping (CLLocation?) -> Void) {
locationCallback = callback
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
manager.requestWhenInUseAuthorization()
locationServicesEnabled = CLLocationManager.locationServicesEnabled()
if locationServicesEnabled {
manager.startUpdatingLocation()
}else {
locationCallback(nil)
}
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locationCallback(locations.last!)
manager.stopUpdatingLocation()
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
didFailWithError = error
locationCallback(nil)
manager.stopUpdatingLocation()
}
deinit {
manager.stopUpdatingLocation()
}
}
And it is called from the object class like this:
class ObjectX: NSObject{
//Class variables
#objc override init() {
let getLocation = LocationService()
getLocation.run {
if let location = $0 {
//get location parameters
}}
Finally the ObjectX class is initiated and used from other place
let getLocation = ObjectX()
//After initiate it I use the object for other purposes, but here the object is not complete, the location parameters have not been populated yet
How can I wait in the class that is calling it until the callback is executed? Should I use getLocation.performSelector()? How?
Maybe this is not the best way to resolve this but it worked for me.
Basically, instead of creating the ObjectX and setting the in the process, the location service is gonna be called the before, then in the callback, the ObjectX is gonna be initialised, then we can set the location parameters for the ObjectX with the location object the we received in the object.
We remove the location set from the initialiser
class ObjectX: NSObject{
//Class variables
#objc override init() {
//Setting the rest of the parameters that are not location
}}
Then class that was initialising the object, we init and run the LocationService, then in callback we create the ObjectX and we set the location parameters
let ls = LocationService()
ls.run { location in
let objectX = ObjectX()
objectX.location = location
//We can use the object here
}
Now I'm working on iOS using RxSwift framework. In my app I have to user user location, but I don't need it to be updated in real time. It's enough if location updated every time user opens app or does some defined action. Therefore, how about implementing singleton class where the last result is cached. Each update by action changes cached result and accepts it to the stream. Stream's default value is cached value. Then, views where user location is needed would subscribe on this stream.
Example implementation using Cache and RxSwift
import Foundation
import Cache
import CoreLocation
import RxSwift
import RxCocoa
class UserLocationManager: NSObject {
private enum Keys: String {
case diskConfig = "Disk Config"
case lastLocation = "User Last Location"
}
// MARK: - Variables
private func cache<T: Codable>(model: T.Type) -> Cache.Storage<T> {
let storage = try! Cache.Storage(diskConfig: DiskConfig(name: Keys.diskConfig.rawValue), memoryConfig: MemoryConfig(expiry: .never), transformer: TransformerFactory.forCodable(ofType: model))
return storage
}
private let locationManager = CLLocationManager()
private var lastPosition: MapLocation? {
get {
do {
return try cache(model: MapLocation.self).object(forKey: Keys.lastLocation.rawValue)
}
catch { return nil }
}
set {
do {
guard let location = newValue else { return }
try cache(model: MapLocation.self).setObject(location, forKey: Keys.lastLocation.rawValue)
}
catch { }
}
}
private let disposeBag = DisposeBag()
static let shared = UserLocationManager()
var locationStream = BehaviorRelay<CLLocationCoordinate2D?>(value: nil)
// MARK: - Methods
func updateLocation() {
if CLLocationManager.locationServicesEnabled() {
locationManager.requestLocation()
}
}
func subscribe() {
locationStream.accept(lastPosition?.clCoordinate2D)
locationStream.subscribe(onNext: { [weak self] location in
guard let `self` = self else { return }
guard let location = location else { return }
self.lastPosition = MapLocation(latitude: location.latitude, longitude: location.longitude)
}).disposed(by: disposeBag)
locationManager.delegate = self
}
// MARK: - Lifecycle
override init() {
super.init()
defer {
self.subscribe()
}
}
}
// MARK: - CLLocationManagerDelegate
extension UserLocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
UserLocationManager.shared.locationStream.accept(location.coordinate)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
}
}
There's no problem conceptually of having a global stream that can be subscribed to. However, your specific implementation is worrisome to me.
One of the cool things about Observable streams is that they are lazy, no work is done unless needed, but you are writing extra code to bypass that feature and I don't think it's necessary. Also, storing there current location when the app goes into the background and just assuming that is a valid location when the app comes back to the foreground (possibly weeks later) sounds inappropriate to me.
The RxCocoa package already has an Rx wrapper for CLLocationManager. It seems to me it would be far simpler to just use it. If you only need one location update then use .take(1). I'd be inclined to add a filter on the accuracy of the location before the take(1).
I read the Radar.io (https://radar.io/documentation/sdk#ios
) iOS SDK documentation and I'm curious how I create a custom
locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) function.
class ViewController: UIViewController, RadarDelegate {
override func viewDidLoad() {
super.viewDidLoad()
Radar.setDelegate(self)
}
}
And
Radar.trackOnce(completionHandler: { (status: RadarStatus, location: CLLocation?, events: [RadarEvent]?, user: RadarUser?) in
// do something with status, location, events, user
})
source: https://radar.io/documentation/sdk#ios
How i get location update and control CLLocationManager in a custom class like this?
protocol MyRadarCloneDelegate {
}
class MyRadarClone {
static func setDelegate(_ delegate:) {
}
static func startTracking() {
}
// some other function for control CLLocationManager
}
If I understand your question correctly you want to access your location from Radar.trackOnce into your custom class.
You can do it this way.
Update your startTracking method this way:
func startTracking(location: CLLocation) {
//do somtrhing with your location
print(location)
}
And into your Radar.trackOnce method add below code:
guard let userLocation = location else {return}
let myRadar = MyRadarClone()
myRadar.startTracking(location: userLocation)
Hope this will help.
Note: Code is not tested.
Don’t create any custom protocols or functions for this
Create a NotificationCenter
Post from locationDidUpdate
Observe where you want
I think this is a best idea
I'm assuming the objective is to create a custom lifecycle method that acts like viewDidLayoutSubviews but is instead locationManagerDidGetLocation, or something, that is called only once in any view controller you put it in. If instead you simply want to extend the location manager's delegate to other view controllers, the strategy is the same (with a small adjustment at the end):
class SomeViewController: UIViewController {
func locationManagerDidGetLocation() {
doSomethingThatRequiresUserLocation()
}
}
CLLocationManagerDelegate has a method for new location data (and one for new location value). Simply post a notification from it:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
NotificationCenter.default.post(name: .locationManagerDidUpdateLocations, object: nil)
}
You can also share the value of the location across your app in a static structure variable or in the app's delegate:
struct CurrentLocation {
static var coordinate: CLLocationCoordinate2D?
}
And update it through the delegate:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// update shared property
let lastLocation = locations.last!
CurrentLocation.coordinate = lastLocation.coordinate
// post app-wide notification
NotificationCenter.default.post(name: .locationManagerDidUpdateLocations, object: nil)
}
To access that value just call CurrentLocation.coordinate anywhere in your app.
Then simply add a notification observer to an object, like a UIViewController, listening for that post:
class SomeViewController: UIViewController {
func addNotificationObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(locationManagerDidUpdateLocationsHandler), name: .locationManagerDidUpdateLocations, object: nil)
}
}
And when the first post arrives, remove the observer, so it only gets called once, and call your custom method:
#objc func locationManagerDidUpdateLocationsHandler() {
NotificationCenter.default.removeObserver(self, name: .locationManagerDidUpdateLocations, object: nil)
locationManagerDidGetLocation()
}
And now it works like a regular lifecycle method that is fired only once when the user's location is available:
class SomeViewController: UIViewController {
func locationManagerDidGetLocation() {
doSomethingThatRequiresUserLocation()
}
}
You can, of course, not remove the observer and have it function like an extended delegate of the location manager. And don't forget to remove all observers in deinit if these view controllers don't persist the life of the app.
I am following Google Places API for IOS tutorial to view the user current place.
I used the same code in the tutorial as follow:
var placesClient: GMSPlacesClient!
// Add a pair of UILabels in Interface Builder, and connect the outlets to these variables.
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var addressLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
placesClient = GMSPlacesClient.shared()
}
// Add a UIButton in Interface Builder, and connect the action to this function.
#IBAction func getCurrentPlace(_ sender: UIButton) {
placesClient.currentPlace(callback: { (placeLikelihoodList, error) -> Void in
if let error = error {
print("Pick Place error: \(error.localizedDescription)")
return
}
self.nameLabel.text = "No current place"
self.addressLabel.text = ""
if let placeLikelihoodList = placeLikelihoodList {
let place = placeLikelihoodList.likelihoods.first?.place
if let place = place {
self.nameLabel.text = place.name
self.addressLabel.text = place.formattedAddress?.components(separatedBy: ", ")
.joined(separator: "\n")
}
}
})
}
But I get the following error in the console:
Pick Place error: The operation couldn’t be completed. The Places API
could not find the user's location. This may be because the user has
not allowed the application to access location information.
NOTE: I have set the NSLocationWhenInUseUsageDescription key (Privacy - Location When In Use Usage Description) in info.plist file.
It's confusing because I followed the tutorial step by step. And am testing the application using physical device with "Locations Services" enabled .
Any idea what I might be doing wrong?
Or is it because the documentation is not up-to-date?
This may be because the user has not allowed the application to access location information.
This points you towards your answer. For Google Places to work you need to request to use location services by calling requestWhenInUseAuthorization(). This will prompts the user to grant permission to the app to use location services.
Please refer to the Apple Docs for more info.
EDIT
You should keep a strong reference to the CLLocationManager that you create so it does not get deallocated when your function exits.
"Create an instance of the CLLocationManager class and store a strong reference to it somewhere in your app.
Keeping a strong reference to the location manager object is required until all tasks involving that object are complete. Because most location manager tasks run asynchronously, storing your location manager in a local variable is insufficient."
Taken from the CLLocationManager Docs
EXAMPLE
class LocationViewController: UIViewController, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
override func viewDidLoad()
{
super.viewDidLoad()
locationManager.delegate = self
if CLLocationManager.authorizationStatus() == .notDetermined
{
locationManager.requestWhenInUseAuthorization()
}
}
}
1. Request user for Location Usage Authorization
requestWhenInUseAuthorization() OR requestAlwaysAuthorization() according to your requirement.
2. In Info.plist, add the following keys:
a. NSLocationAlwaysUsageDescription OR NSLocationWhenInUseUsageDescription
b. NSLocationAlwaysAndWhenInUseUsageDescription
Example:
class ViewController: UIViewController, CLLocationManagerDelegate
{
let locationManager = CLLocationManager()
override func viewDidLoad()
{
super.viewDidLoad()
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus)
{
}
}
In my app delegate class, i am trying to retrieve user Current Location from another class using delegate. This retreived User Curren location will be used in many parts of my application.So ,i have set it here in AppDelegate Class
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var helperLocation:HelperLocationManager?
var currentLocation:CLLocation?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
//get the current location and set it so all other screens can use it
self.helperLocation = HelperLocationManager()
self.helperLocation?.delegate = self
return true
}
}
extension AppDelegate: SendLocationDelegate{
func sendCoOrdinates(loccoordinate:CLLocation, placemark:CLPlacemark){
currentLocation = loccoordinate
}
}
And this is what seems to be my HelperLocationManager Class
protocol SendLocationDelegate{
func sendCoOrdinates(coordinates:CLLocation,placemark:CLPlacemark)
}
class HelperLocationManager: NSObject {
var locationManager = CLLocationManager()
var delegate:SendLocationDelegate?
override init() {
super.init()
var code = CLLocationManager.authorizationStatus()
if code == CLAuthorizationStatus.NotDetermined {
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
}
locationManager.delegate = self
}
}
extension HelperLocationManager: CLLocationManagerDelegate{
func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
switch status {
case CLAuthorizationStatus.Restricted:
println( "Restricted Access to location")
case CLAuthorizationStatus.Denied:
println( "User denied access to location please turn on the location")
// UIApplication.sharedApplication().openURL(NSURL(string: UIApplicationOpenSettingsURLString)!)
//may be open here settings page and say to turn on the setting location services
default:
locationManager.startUpdatingLocation()
}
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
var locValue = locations.last as! CLLocation
locationManager.stopUpdatingLocation()
CLGeocoder().reverseGeocodeLocation(manager.location, completionHandler: {(placemarks,error)-> Void in
if (error != nil) {
println("Reverse geocoder failed with error" + error.localizedDescription)
return
}
if placemarks.count > 0 {
let pm = placemarks[0] as! CLPlacemark
self.delegate?.sendCoOrdinates(locValue,placemark: pm)
} else {
println("Problem with the data received from geocoder")
}
})
}
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
println("Your error is ", error.localizedDescription)
}
}
I made my call back method to trigger if there is any change in the user location...
Everything is fine. HelperLocationManager class sends the current location to the method sendCoOrdinatesthat is implemented in AppDelegate And I have set the current location and now i am accessing these location from presentedViewController as
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
pickUpDistanceLocation = appDelegate.currentLocation
My problem is when i try to access the value very fast enough in another class during the time interval the delegate call back method doesnot send me my current Location.I get nil in this case.but if i wait 2-3 sec and go to another class i get the value from delegate.
Can anyone explain me what am i doing wrong?
This is an architectural issue - you say:
when i try to access the value very fast enough in another class during the time interval the delegate call back method does not send me my current Location
You have to invert that - rather than checking the current location, with the risk of not having one because it's not been obtained yet, you should let the HelperLocationManager notify when it has a location (the hollywood principle: don't call me, I'll call you).
This can be done in different ways:
using the delegation pattern
using an event bus (which can be implemented with NSNotificationCenter)
using callbacks
There are of course many other ways to achieve the same result.
The delegation pattern is probably not the best solution when there are more than one observer.
I would use the callback way, with a subscriber registering to location updates by providing a closure to HelperLocationManager.
HelperLocationManager can store all callbacks into an array, and invoke each of them when a location update is available. Optionally, it can invoke a closures right after registration, if a location is already available.
Last, the subscriber must be able to unsubscribe, so HelperLocationManager should expose a method which removes a callback from its internal list.
This is just an idea - as said, it can be done in several different ways, the common rule is to just invert how the location is passed.
Note: I would make HelperLocationManager a singleton, and remove it from AppDelegate. If I want to use HelperLocationManager, I should contact it directly, instead of having to access through a 3rd party (the app delegate).