I'm trying to use a Location Protocol to enable the mocking of CLLocationManager & CLLocationManager Delegate functions. I've implemented a completion handler (via a web explainer) to be able to return a location via a delegate however the competition never returns (the delegate does return the correct value).
When writing this post the code doesn't seem to make sense, however, I'm not sure how to solve it in this kind of structure.
Call:
func test_getCurrentLocation_returnsExpectedLocation() {
var locationManagerMock = LocationManagerMock()
locationManagerMock.locationToReturn = { return CLLocation(latitude: 10.0, longitude: 10.0)}
let sut = LocationService(locationManager: locationManagerMock)
let expectedLocation = CLLocation(latitude: 10.0, longitude: 10.0)
let completionExpectation = expectation(description: "completion expectation")
sut.getCurrentLocation { (location) in
XCTAssertEqual(location.coordinate.latitude, expectedLocation.coordinate.latitude)
XCTAssertEqual(location.coordinate.longitude, expectedLocation.coordinate.longitude)
completionExpectation.fulfill()
}
wait(for: [completionExpectation], timeout: 1)
}
Location Manager with completion
func getCurrentLocation(completion: #escaping (CLLocation) -> Void) {
self.locationManager.requestLocation()
self.currentLocationCallback = { (location) in
completion(location)
}
}
Delegate write to class var
func locationManager(_ manager: LocationManagerInterface, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
self.currentLocationCallback?(location)
print("Content from Delegate")
print(location)
self.currentLocationCallback = nil
}
Related
I'm working on an App Shortcut using the new AppIntents framework in iOS 16 and I'm trying to get the user's current location, everything is enabled and set-up correctly with the permissions
func perform() async throws -> some IntentResult {
//Request User Location
IntentHelper.sharedInstance.getUserLocation()
guard let userCoords = IntentHelper.sharedInstance.currentUserCoords else { throw IntentErrors.locationProblem }
//How to wait for location??
return .result(dialog: "Worked! Current coords are \(userCoords)") {
IntentSuccesView()
}
}
And here is the IntentHelper class
class IntentHelper: NSObject {
static let sharedInstance = IntentHelper()
var currentUserCoords: CLLocationCoordinate2D?
private override init() {}
func getUserLocation() {
DispatchQueue.main.async {
let locationManager = CLLocationManager()
locationManager.delegate = self
print("FINALLY THIS IS IT")
self.currentUserCoords = locationManager.location?.coordinate
print(self.currentUserCoords)
}
}
}
extension IntentHelper: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
manager.stopUpdatingLocation()
}
}
Problem is, this sometimes, very rarely works, most of the times it prints nil, so how would you go about waiting for the location?
The problem is you are trying to get the location synchronously, so it only works if locationManager.location was already not nil by the time you ask for it. Instead this operation may take time and is therefore asynchronous.
So the basic flow is like this:
Check permissions (yes, you have to do it every time, as user may take away the permissions a any point)
And tell CLLocationManager to start resolving user location
After that just listen for result via locationManager(:, didUpdateLocations:) event of the CLLocationManagerDelegate, which
you need to implement (in your case in the same class, as you already
implemented the failure case in extension).
On top of that, you probably want to wait for location update (either coordinates or failure) inside your func perform().
So I would say you need to have something like this in func perform():
// Wait for coordinates
guard let userCoords = await IntentHelper.sharedInstance.getCurrentCoordinates() else { ... }
where the getCurrentCoordinates() is just an async wrapper, something like:
func getCurrentCoordinates() async -> CLLocationCoordinate2D? {
await withCheckedContinuation { continuation in
getCurrentCoordinates() { coordinates in
continuation.resume(returning: coordinates)
}
}
}
while getCurrentCoordinates(callback:) will be something like:
class IntentHelper {
var callback: ((CLLocationCoordinate2D?) -> Void)?
//...
func getCurrentCoordinates(callback: #escaping (CLLocationCoordinate2D?) -> Void) {
// Step 1: check permissions
let status = CLLocationManager.authorizationStatus()
guard status == .authorizedAlways || status == .authorizedWhenInUse else {
// you can't ask for permissions
callback(nil)
return
// Step 2: preserve callback and request location
self.callback = callback
locationManager?.requestLocation()
}
}
Now all you need to do is wait for locationManager(:, didUpdateLocations:) or locationManager(:, didFailWithError:) to happen:
extension IntentHelper: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// Pass the result (no location info) back to the caller
self.callback?(nil)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// Pass the result location back to the caller
// For simplicity lets say we take the first location in list
self.callback?(locations.first)
}
}
Note: this is a draft code, I didn't try to compile it, so you may need to fix some compilation errors.
Here's a nice walkthrough of the whole scenario (which also shows a nicer code organization (i.e. how to ask for permissions, etc).
I have a simple CLLocationManager implementation that works in one project but not in my new project.
The code is almost identical but I cannot get the .didUpdateLocations function to call. My code is below. Any ideas why I cannot get the update to work? I'm at a loss, I've build many apps using location services and never seen this situation.
Also I have the three settings in the PLIST set correctly for Privacy-Location Always etc.
There are no errors given, it simply doesn't call .didUpdateLocations
Weather Class
class DarkSkyWeatherController: UIViewController, CLLocationManagerDelegate {
var weatherGetterDelegate: DarkSkyWeatherControllerDelegate?
var locationManager = CLLocationManager()
var lat = String()
var long = String()
func getLocation() {
// Ask for Authorisation from the User.
locationManager.requestAlwaysAuthorization()
// For use in foreground
locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
locationManager.startUpdatingLocation()
}
locationManager.delegate = self
locationManager.requestLocation()
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else {return}
print("locations = \(locValue.latitude) \(locValue.longitude)")
lat = String(locValue.latitude)
long = String(locValue.longitude)
getDarkSkyWeather { (fetchedInfo) in
if let myFetchedInfo = fetchedInfo {
self.weatherGetterDelegate?.getMyWeather(weather: myFetchedInfo)
}
}
}
ViewDidLoad in main window
let weather = DarkSkyWeatherController()
weather.weatherGetterDelegate = self
weather.getLocation()
Thanks for looking at this.
Without seeing your full main window code, I bet that the problem is with the scope and lifecycle of your controller:
override func viewDidLoad() {
let weather = DarkSkyWeatherController()
weather.weatherGetterDelegate = self
weather.getLocation()
// Function exits. The weather constant dies off.
// This is why you don't get callbacks.
}
Do the following, instead.
let weather = DarkSkyWeatherController()
override func viewDidLoad() {
weather.weatherGetterDelegate = self
weather.getLocation()
// Function exits, but the weather constant lives on as a field of your main ViewController. You'll get your callbacks now.
}
I've developing an indipendent WatchOS app whose aim is identifying when an user leaves a specific area, sending consequentially a notification. In order to do that, the application heavily relies on background location updates.
So far, app is working fine. It fetches position based on distanceFilter property of CLLocationManager. The problem is battery. The approach I followed keep background location updates in execution, even though they're fetched only when a specific distance is "covered".
My idea then was to disable location update when the area is left by the user, and also disable this service during night hours. However, I'm facing serious problem with this type of approach.
My main problem is that disabling location update while in background does not allow me to resume it. I tried doing this with:
A Timer.
scheduleBackgroundRefresh(withPreferredDate:userInfo:scheduledCompletion:) method, calling startLocationUpdates() in the delegate
Nothing seems to work. My question is:
There is a way for resume background location updates if it was previously disabled?
Thank you in advance.
UPDATE n.2: I've tried to execute location updates with WKApplicationRefreshBackgroundTask but it just ignore requestLocation() function (suggested by #RomuloBM)
//In extension delegate handle() function
case let backgroundTask as WKApplicationRefreshBackgroundTask:
// Be sure to complete the background ta
LocMng = LocationManager() // I even tried to create a new element!
LocMng.LocMng.requestLocation()// it is just ignored
backgroundTask.setTaskCompletedWithSnapshot(false)
I call a background task with this function in my LocationManager:
//In didUpdateLocation
if background {
WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate: Date(timeIntervalSinceNow: 30), userInfo: nil){ _ in
print("Done")
self.background = false
self.LocMng.stopUpdatingLocation()
}
}
For reference, here is my LocationManager class:
enum ScanningMode {
case Precise
case Normal
}
class LocationManager : NSObject, CLLocationManagerDelegate, UNUserNotificationCenterDelegate, ObservableObject {
let LocMng = CLLocationManager()
let NotMng = UNUserNotificationCenter.current()
var modeOfScanning: ScanningMode!
var region: CLCircularRegion!
var previousLocation: CLLocation!
// variables for position...
override init() {
super.init()
// stuff for my app...
modeOfScanning = .Precise
setupManager()
setupNotification()
startLocalization()
}
private func startLocalization(){
switch modeOfScanning!{
case ScanningMode.Precise:
LocMng.desiredAccuracy = kCLLocationAccuracyBest
LocMng.distanceFilter = 15
case ScanningMode.Normal:
LocMng.desiredAccuracy = kCLLocationAccuracyHundredMeters
LocMng.distanceFilter = 80
}
LocMng.startUpdatingLocation()
}
private func setupManager(){
LocMng.requestAlwaysAuthorization()
LocMng.delegate = self
LocMng.desiredAccuracy = kCLLocationAccuracyBest
}
private func setupNotification(){
NotMng.delegate = self
NotMng.requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in
if granted {
print("NotificationCenter Authorization Granted!")
}
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == CLAuthorizationStatus.authorizedAlways{
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
LocMng.allowsBackgroundLocationUpdates = true
// For the sake of clarity, I will cut out this chunk of code and
// just showing how I execute my action based on the result of location
// This is just an example
actualLocation = locations[length-1]
//find if in a forget
if previousLocation != nil{
if !region.contains(actualLocation!.coordinate) && region.contains(previousLocation!.coordinate){
//Schedule notification
LocMng.stopUpdatingLocation() // <- this does not allow me to resume
}
}
previousLocation = actualLocation
}
}
I'm trying to retrieve the user's location in the app using a location manager; as explained in Apple's documentation I created the following method:
func startReceivingLocationChanges() {
let authorizationStatus = CLLocationManager.authorizationStatus()
if authorizationStatus != .authorizedWhenInUse && authorizationStatus != .authorizedAlways {
locationManager.requestWhenInUseAuthorization()
startReceivingLocationChanges()
return
}
if !CLLocationManager.locationServicesEnabled() {
displayError(withTitle: "Location Not Available", withDescription: "Enable Location Services at Settings > Privacy > Location Services", sender: self)
return
}
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 100.0 // In meters.
locationManager.pausesLocationUpdatesAutomatically = true
locationManager.activityType = .other
locationManager.delegate = self
locationManager.startUpdatingLocation()
}
But When I launch the app, this crash showing the error "Thread 1: EXC_BAD_ACCESS (code=2, address=0x16f0a7f60)" near the line:
locationManager.requestWhenInUseAuthorization()
I specify that I added the relative "Privacy - Location Always and When In Use Usage Description" and "Privacy - Location When In Use Usage Description" keys inside the info.plist.
Does anyone know what causes the issue? Thanks.
There are couple of mistakes in your code like recursion in case user don’t give the permission for when in use so look at my code. Assuming you had added "Privacy - Location Always and When In Use Usage Description" and "Privacy - Location When In Use Usage Description" keys inside the info.plist. Below code works perfect without any issue and apple recommended.
import Foundation
import CoreLocation
typealias LocateMeCallback = (_ location: CLLocation?) -> Void
class LocationTracker: NSObject {
static let shared = LocationTracker()
var locationManager: CLLocationManager = {
let locationManager = CLLocationManager()
locationManager.activityType = .automotiveNavigation
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
locationManager.distanceFilter = 10
return locationManager
}()
var locateMeCallback: LocateMeCallback?
var currentLocation: CLLocation?
var isCurrentLocationAvailable: Bool {
return currentLocation != nil
}
func enableLocationServices() {
locationManager.delegate = self
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
// Request when-in-use authorization initially
locationManager.requestWhenInUseAuthorization()
case .restricted, .denied:
// Disable location features
print("Fail permission to get current location of user")
case .authorizedWhenInUse:
// Enable basic location features
enableMyWhenInUseFeatures()
case .authorizedAlways:
// Enable any of your app's location features
enableMyAlwaysFeatures()
}
}
func enableMyWhenInUseFeatures() {
locationManager.startUpdatingLocation()
}
func enableMyAlwaysFeatures() {
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = true
locationManager.startUpdatingLocation()
}
func locateMe(callback: #escaping LocateMeCallback) {
self.locateMeCallback = callback
enableLocationServices()
}
private override init() {}
}
// MARK: - CLLocationManagerDelegate
extension LocationTracker: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
print("locations = \(location.coordinate.latitude) \(location.coordinate.longitude)")
locateMeCallback?(location)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
enableLocationServices()
}
}
Usage
LocationTracker.shared.locateMe { location in
guard let location = location else {
print("Cann't retrieve current location of user")
return
}
// do what ever you want to do with location
}
Found the issue, it was related somehow to the recursive call to the function
startReceivingLocationChanges
inside the function itself. I solved it asking for location permissions inside ViewDidLoad method and removing the recursive call.
So in my current project Im doing a method which calculates the saved emission when driving a moped compared to a average car. The function contains two parts, the method (the calculation) and the tracker function. The main problem is that the tracker function somehow does not seem to track at all.
My main question is, how do I get the tracker function to always track while the app is on?
This is the tracker function
var startLocation:CLLocation!
var lastLocation: CLLocation!
var traveledDistance:Double = 0
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if startLocation == nil {
startLocation = locations.first
} else {
if let lastLocation = locations.last {
let distance = startLocation.distanceFromLocation(lastLocation)
let lastDistance = lastLocation.distanceFromLocation(lastLocation)
traveledDistance += lastDistance
print( "\(startLocation)")
print( "\(lastLocation)")
print("FULL DISTANCE: \(traveledDistance)")
print("STRAIGHT DISTANCE: \(distance)")
var travelDistance = setData("distance")
}
}
lastLocation = locations.last
}
And this is the method
func calculateEmission(numbers: Int...) -> Double{
let recordedDistance = getData("distance")
let dis = recordedDistance
let emissionAve = 0.16
let calculatedEmission : Double = Double(dis) * Double(emissionAve)
print(calculatedEmission, "kg Co2")
return calculatedEmission
}
Make sure you have the following in your info.plist. Then you should get prompted to allow access to the location services.
<key>NSLocationAlwaysUsageDescription</key>
<string>Needs access to access GPS</string>
<key>NSLocationUsageDescription</key>
<string>Needs access to access GPS</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Needs access to access GPS</string>
You should have some thing like this in viewDidLoad.
override func viewDidLoad() {
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
}