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
}
}
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 am trying to implement directions with clLocationManager in a project.
Everything is working ok, but the didEnterrRegion function is very slow to fire.
When testing, I enter the region but only 2-3 minutes after exiting the region I get the callback. Does anyone have any suggestion on how to improve this?
This is the locationManager:
private lazy var locationManager: CLLocationManager = {
let locationManager = CLLocationManager()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
locationManager.distanceFilter = kCLDistanceFilterNone
handleAuthorizationStatus(locationManager: locationManager)
} else {
//TODO: Handle error
}
return locationManager
}()
This is which regions I am tracking, here I am also drawing each region to easier see when I enter specific region:
private func getRouteSteps(_ mapView: MKMapView, route: MKRoute) {
for monitoredRegion in locationManager.monitoredRegions {
locationManager.stopMonitoring(for: monitoredRegion)
}
let steps = route.steps
self.steps = steps
for i in 0..<steps.count {
let step = steps[i]
let region = CLCircularRegion(center: step.polyline.coordinate, radius: 30, identifier: "\(i)")
let circle = MKCircle(center: region.center, radius: region.radius)
mapView.addOverlay(circle)
locationManager.startMonitoring(for: region)
}
stepCounter += 1
let initialMessage = "Om \(Int(steps[stepCounter].distance)) meter \(steps[stepCounter].instructions.lowercased())"
directionMessage = initialMessage
}
This is the locationManager-function:
extension MapViewModel: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// manager.stopUpdatingLocation()
if directionsViewState != .isShowingRoute {
if let location = locations.last {
self.didUpdateRegion = "Updated region with accuracy: \(location.horizontalAccuracy)"
let center = location.coordinate
setNewRegionForMapView(center: center)
isCenteringUserLocation = true
}
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
handleAuthorizationStatus(locationManager: locationManager)
}
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
self.didEnterregion = "Entered region: \(region.identifier)"
stepCounter += 1
if stepCounter < steps.count {
let message = "Om \(Int(steps[stepCounter].distance)) meter \(steps[stepCounter].instructions.lowercased())"
directionMessage = message
let speechUtterance = AVSpeechUtterance(string: message)
speechSynthesizer.speak(speechUtterance)
} else {
directionMessage = "You have arrived at your destination!"
stepCounter = 0
let speechUtterance = AVSpeechUtterance(string: directionMessage)
speechSynthesizer.speak(speechUtterance)
for monitoredRegion in locationManager.monitoredRegions {
locationManager.stopMonitoring(for: monitoredRegion)
}
}
}
}
I am also calling locationManager.startUpdatingLocations in the init-method.
Suppose that you are using locationManager.requestWhenInUseAuthorization().
So, I have some suggestions for you:
[METHOD 1] Apply background location updating:
Enable background mode to your target
Then add these lines of code:
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
The line allowsBackgroundLocationUpdates = true will allow your app to run in the background to receive new location event (ex: lock screen, use another app,...).
And, pausesLocationUpdatesAutomatically = false will tell the system to not pause location updating, system can pause background location updating to save battery.
Call your locationManager.startUpdatingLocation() to start listening for new location change.
Next [METHOD 2], if above method doesn't work, you can switch to use locationManager.requestAlwaysAuthorization(). Its description is Requests the user’s permission to use location services regardless of whether the app is in use. System can wake your app to run in background to handle new location events. Remember to add the permission description for requestAlwaysAuthorization in Info.plist file.
Next [METHOD 3], try to increase your CLCircularRegion's radius to higher value, ex: 50 meters. Or you can try to increase the distanceFilter to 2 meters, distanceFilter = none isn't a best option.
Finally [METHOD 4], Make your customized region monitoring logic by calculating distance from user's location to region's center whenever we get a new location event, use this one func distance(from location: CLLocation) -> CLLocationDistance. If distance is <= your region's radius, that means user already crossed the boundary.
Hope that you can solve your problem.
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 use location collection, I want to achieve the effect of the following images, when the collection location is executed and executed in the background, you can display your own app in the status column.
I thought that as soon as I used the location and background, he would automatically display it. But not as I thought.
The following is my code
import UIKit
import CoreLocation
class ViewController: UIViewController , CLLocationManagerDelegate{
var locationMgr : CLLocationManager!
override func viewDidLoad() {
super.viewDidLoad()
locationMgr = CLLocationManager()
locationMgr.delegate = self
locationMgr.desiredAccuracy = kCLLocationAccuracyBestForNavigation
locationMgr.allowsBackgroundLocationUpdates = true
locationMgr.pausesLocationUpdatesAutomatically = true
if CLLocationManager.authorizationStatus() != .authorizedAlways {
locationMgr.requestAlwaysAuthorization()
}else{
locationMgr.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let a = locations.last{
print("----location update----")
print(a.coordinate.latitude);
print(a.coordinate.longitude);
print("---------------")
}
}
}
I can get the updated data when I execute it, but I don't see the display of the status bar above.
I don't know what I missed?
The alert on the top shows when you app has permission to access the location when it is in foreground and uses when the app is in background.
Use
if CLLocationManager.authorizationStatus() != .authorizedWhenInUse {
locationMgr.requestWhenInUseAuthorization()
}else{
locationMgr.startUpdatingLocation()
}
instead of
if CLLocationManager.authorizationStatus() != .authorizedAlways {
locationMgr.requestAlwaysAuthorization()
}else{
locationMgr.startUpdatingLocation()
}
Why is the Location Manager calling startUpdatingLocation more than once? Sometimes it is calling once, other times it is calling it three times. I don't know why; maybe you could help me. I have this code from GitHub.
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate
{
let locationManager = CLLocationManager()
override func viewDidLoad()
{
super.viewDidLoad()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error) -> Void in
if (error != nil)
{
print("Error: " + error!.localizedDescription)
return
}
if placemarks!.count > 0 {
if let pm = placemarks?.first {
self.displayLocationInfo(pm)
}
}
else
{
print("Error with the data.")
}
})
}
func displayLocationInfo(placemark: CLPlacemark)
{
self.locationManager.stopUpdatingLocation()
print(placemark.locality)
print(placemark.postalCode)
print(placemark.administrativeArea)
print(placemark.country)
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError)
{
print("Error: " + error.localizedDescription)
}
}
Yes, this is standard behavior. When you start location services you will generally receive a series of increasingly accurate CLLocation updates (i.e. with horizontalAccuracy decreasing over time) as the device "warms up". For example, it might start reporting location information that it might already have on the basis of cell towers, but as the GPS chip gets more information by which it can better triangulate your location, it will give you updates. Etc.
If you want to reduce this behavior, you can use a combination of a larger distanceFilter, a lower desiredAccuracy, or call stopUpdatingLocation once you get a location that you will geocode.
Right now you are calling stopUpdatingLocation, but you're doing it from the asynchronously called closure of reverseGeocodeLocation. This means that more location updates are able to slip in before the completion handler of reverseGeocodeLocation is called. If you call stopUpdatingLocation synchronously (e.g. before reverseGeocodeLocation), then you will avoid this behavior.