I'm developing an application which is tracking the location of the phone on time interval chosen by the user. I made it with timer which starts startUpdatingLocation, when it get the location stops updating sends the location to the server and starts timer again. Everything is done in background.
I need to get location data in background mode (in iOS 8) even after phone restarts and send the data to a server. I tried hundreds methods and no one works for me. So ... what i've got at this moment:
In info.plist:
Required background modes - location and fetch
NSLocationAlwaysUsageDescription - Location is required for this application to work properly
In AppDelegate:
var locationController: LocationController = LocationController() as LocationController;
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
UIApplication.sharedApplication().setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum);
var bgTask = UIBackgroundTaskIdentifier()
bgTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler { () -> Void in
UIApplication.sharedApplication().endBackgroundTask(bgTask)
}
if(self.locationController.profileForTracking != nil && self.locationController.profileForTracking != "Disabled" && self.locationController.intervalForTracking != nil && self.locationController.trackingAllowed == true){
self.locationController.initLocationManager();
if(self.locationController.timer != nil){
self.locationController.timer = self.locationController.timer;
} else {
self.locationController.startTimerForLocationUpdate();
}
println("Location can now start ....");
}
return true
}
func application(application: UIApplication!, performFetchWithCompletionHandler completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
var bgTask = UIBackgroundTaskIdentifier()
bgTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler { () -> Void in
UIApplication.sharedApplication().endBackgroundTask(bgTask)
}
if(self.locationController.profileForTracking != nil && self.locationController.profileForTracking != "Disabled" && self.locationController.intervalForTracking != nil && self.locationController.trackingAllowed == true){
self.locationController.initLocationManager();
if(self.locationController.timer != nil){
self.locationController.timer = self.locationController.timer;
} else {
self.locationController.startTimerForLocationUpdate();
}
println("Location can now start ....");
}
}
Everything seems to work (when app is in foreground or background/inactive) except when the phone was restarted or user terminate application. I think I'm doing something wrong and iOS does not wake up the application. How can I do it, and if I can do it somehow is there a way so get the location in time period chosen by the user (30 minutes, 1 hour ...). Thanks in advance!
While moving to background or when app terminated call this method
startMonitoringSignificantLocationChanges();
So if you reboot your device or app got killed, whenever there is significant location change in your location, OS will launch your app in background.
As per apple document
If you start this service and your app is subsequently terminated, the
system automatically relaunches the app into the background if a new
event arrives. In such a case, the options dictionary passed to the
application:willFinishLaunchingWithOptions: and
application:didFinishLaunchingWithOptions: methods of your app
delegate contains the key UIApplicationLaunchOptionsLocationKey to
indicate that your app was launched because of a location event. Upon
relaunch, you must still configure a location manager object and call
this method to continue receiving location events. When you restart
location services, the current event is delivered to your delegate
immediately. In addition, the location property of your location
manager object is populated with the most recent location object even
before you start location services.
What you want is no longer possible. As of iOS7, an application will only be kept alive in the background to receive locations if it was launched into the foreground by the user, not by the system (such as via a significant location change event). This change was mentioned briefly in in one of the talks at WWDC '13, probably the one titled "What's New in Core Location".
I had an app with this functionality, and it broke in iOS7. I spoke with Apple developer support, and they confirmed the change. The project I was working on was a origin-destination survey intended for an academic study; our solution ended up being to keep a set of local notifications, and regularly update their fire times into the future while we are running; if we ever stop running the user gets a notification to relaunch the app.
Be aware, as well, that if you ever tell core location to stop updating your location, you will not be able to turn that back on until you are again launched into the foreground by the user. The solution we used here is a combination of deferred location updates, and of turning the 'desired accuracy' way up, so that the phone isn't powering on the GPS or wifi antennas when it isn't necessary. This isn't ideal, obviously, but our battery performance has managed to be more reasonable then I had expected.
Related
I'm implementing Personal VPN by PacketTunnel. Through startVPNTunnel method of NETunnelProviderManager, i checked whether VPN Connection is run well.
However, I have a problem. I added the exit code of vpn connection in applicationWillTerminate to disable vpn connection when app is terminated like following code below. But it doesn't work.
If test this code, loadAllFromPreferences is run but callback function of loadAllFromPreferences isn't called. This code runs well anywhere except applicationWillTerminate. Why it doesn't work? Is there any way to disable vpn when app is terminated?
func applicationWillTerminate(_ application: UIApplication) {
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
var manager:NETunnelProviderManager?=nil;
for m in managers! {
if m != nil {
if m.localizedDescription == "profile" {
manager = m;
break
}
}
}
manager?.connection.stopVPNTunnel()
}
}
As per Apple's documentation of applicationWillTerminate(application:):
Your implementation of this method has approximately five seconds to perform any tasks and return. If the method does not return before time expires, the system may kill the process altogether.
If applicationWillTerminate is called but the closure is not make sure that it does not take more than "approximately" (sic) 5 seconds.
I have not used the libraries you are using but from a general point of view there are probably better strategies than iterating over all available managers. Like storing a reference/identifier to a given manager when starting a connection and using that reference/identifier to terminate it.
I'm making a ibeacon region monitoring app with location updates when the user enter into this region (app not in foreground). This location updates must be configured as kCLLocationAccuracyBestForNavigation accuracy (because I have to make a tracking while the use remain in the region,
subscribe to me significant changes is not enough). Everything works well, but after 20 seconds (sometimes 1 minute o more) I stop receiving locations updates. I put all the keys in info.plist for always location usage, I include the background modes in capabilities section and locations updates on background.
I configure the locationManager with different configurations and always the SO stops my locations updates. I'm using IOS 12 and Iphone 7 for testing.
The way I configure CLLocationManager:
self.locationManager.desiredAccuracy
=kCLLocationAccuracyBestForNavigation
self.locationManager.activityType = .automotiveNavigation
self.locationManager.distanceFilter = kCLDistanceFilterNone;
self.locationManager.delegate = self
self.locationManager.allowsBackgroundLocationUpdates = true
self.locationManager.pausesLocationUpdatesAutomatically = false
Start location updates (when user enter in Ibeacon Region):
func beaconManager(_ manager: KTKBeaconManager, didEnter region:
KTKBeaconRegion) {
self.locationManager.startUpdatingLocation()
}
And finally, in didUpdate locations i call a web service:
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
//Call web service using alamofire
}
I ask for your help to know if I am performing the settings correctly for the purpose I want to perform and any clue that lets me know why the operating system kills my process to get locations updates
Getting regular location updates in the background on iOS is tricky. The operating system is designed to keep apps from constantly running in the background to optimize battery usage, and it suspends them after a period of time unless you have several things exactly right.
You need to do three things:
You must get obtain always location permission from the user (as you say you've done).
You must add the following entry to your Info.plist. This will allow your app to run indefinitely in the background, however if you wish to submit your app to the App Store, this entry will also declare to reviewers that it is a background location app, and you will need to convince them that it provides a location-based benefit to the user, and that the user is aware of this benefit.
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
You must maintain a background thread to keep your app alive. It doesn't matter if you actually do anything in this background thread. Just having it be active keeps iOS from suspending your app.
var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
func extendBackgroundRunningTime() {
if (backgroundTask != UIBackgroundTaskIdentifier.invalid) {
// already started
return
}
NSLog("Attempting to extend background running time")
self.backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "DummyTask", expirationHandler: {
NSLog("Background task expired by iOS. Cannot run again until a new beacon region event")
UIApplication.shared.endBackgroundTask(self.backgroundTask)
self.backgroundTask = UIBackgroundTaskIdentifier.invalid
})
DispatchQueue.global().async {
while (true) {
let backgroundTimeRemaining = UIApplication.shared.backgroundTimeRemaining
// This will be a very large number if you have proper permissions
// If not, it will generally count down from 10 seconds once you are in the
// background until iOS suspends your app.
NSLog("Thread background time remaining: \(backgroundTimeRemaining)")
Thread.sleep(forTimeInterval: 1.0)
}
}
}
Issue: Different Behavior In 3 Different Contexts
Ok so Ok, in iOS it seems three different things can happen regarding Push Notifications:
When a Push Notification is received when the app is not in the foreground
something shows up in Notification Center
if the app is opened by tapping the notification, either AppDelegate.DidReceiveRemoteNotification(...) or AppDelegate.ReceivedRemoteNotification(...) is called, apparently depending on which one is implemented (??).
if the app is opened without tapping the notification, only AppDelegate.WillEnterForeground(...), is called, without any explicit mention of the notification, and nothing else happens to acknowledge that a notification was received.
When a Push Notification is received when the app is in the foreground it causes the UNUserNotificationCenterDelegate, if there is one, to execute UNUserNotificationCenterDelegate.WillPresentNotification(...).
Approach: Routing To One Method From All Contexts
So to cover all bases with Push I need to implement something in all three methods: AppDelegate.DidReceiveRemoteNotification(...) / AppDelegate.ReceivedRemoteNotification(...), AppDelegate.WillEnterForeground(...), and UNUserNotificationCenterDelegate .WillPresentNotification(...).
Here are some stubs to show my approach to all this.
First, I created a custom UNUserNotificationCenterDelegate, with a Shared static member:
public class IncomingNotificationHandler : UNUserNotificationCenterDelegate
{
public static IncomingNotificationHandler Shared = new IncomingNotificationHandler();
...
}
Second, inside that class I made a handler that I can route to in every case (again, this is just a stub for debugging purposes):
//sets all parameters to null by default, so it can be called from methods
//that don't know anything about notifications:
public void HandleNotificationsIfAny(UIApplication application = null,
NSDictionary userInfo = null,
Action<UIBackgroundFetchResult> completionHandler = null)
{
//checks if userInfo is null, and logs its conclusions about that:
if (userInfo == null)
{
//In the null case, we can get pending notifications from
//UNUserNotificationCenter:
UNNotification[] pendingNotifications = new UNNotification[] { };
UNUserNotificationCenter.Current.GetDeliveredNotifications(returnedValue => pendingNotifications = returnedValue);
//Then we log the number of pending notifications:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): delivered notification count: " + pendingNotifications.Length);
//And make note of where this was probably called from:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): may have been called from this.WillPresentNotification(...) OR AppDelegate.WillEnterForeground(...)");
return;
});
}
else
{
//In the non-null case, we log the userInfo
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): just got info: " + userInfo);
//And make note of where this was probably called from:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): may have been called from AppDelegate.DidReceiveRemoteNotification(...)");
}
}
Third, inside the same class, I implemented the single method that's required by UNUserNotificationCenterDelegate, and I routed to the handler from it:
public override void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
HandleNotificationsIfAny();
}
Fourth, and last, inside AppDelegate, I routed to the same handler from both relevant methods:
//I prefer using DidReceiveRemoteNotification because in my experience
//the other one is sometimes not reliable:
public override void DidReceiveRemoteNotification(UIApplication application,
NSDictionary userInfo,
Action<UIBackgroundFetchResult> completionHandler)
{
//Simply passing on all the parameters called in this method:
IncomingNotificationHandler.Shared.HandleNotificationsIfAny(application, userInfo, completionHandler);
}
//WillEnterForeground also calls the handler without any parameters
//because it doesn't automatically know anything about notifications:
public override void WillEnterForeground(UIApplication application)
{
IncomingNotificationHandler.Shared.HandleNotificationsIfAny();
}
With that, as it stands, I think I'm handling a notification event in the same way no matter how my app is alerted about it, and even when it's not alerted at all.
Does anyone know if I now have it covered, or if there's some other cases I need to handle?
For the first scenario: AppDelegate.ReceivedRemoteNotification
It reflects the objective c method: application:didReceiveRemoteNotification:, but this event has been deprecated since iOS 10: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623117-application?language=objc. So I think there's no need to handle this event.
For the second scenario: AppDelegate.DidReceiveRemoteNotification
You can still utilize it to handle notifications now if you haven't implemented UNUserNotificationCenter and please notice it is only valid after iOS 7+. Moreover, this event will be triggered when app is on the foreground and if your app is on the background, this event only fires when the user clicks the notification to open your application. And there's no way to access the notification's information if the user clicks the icon to open the app.
I don't think handling AppDelegate.WillEnterForeground is a good approach, as it will be called each time the app resumes from background to foreground even though there are no notifications.
For the scenario: UNUserNotificationCenterDelegate
You could only use this feature after iOS 10. Once you have implemented it on the device iOS 10+, DidReceiveRemoteNotification and ReceivedRemoteNotification will never be triggered. WillPresentNotification will be called when app is on the foreground. DidReceiveNotificationResponse will be fired when the app is on the background and user clicks notifications to open it.
As a conclusion, if you want to easily handle the notification AppDelegate.DidReceiveRemoteNotification is enough. If you want to consume the new features of UNUserNotificationCenter, AppDelegate.DidReceiveRemoteNotification and UNUserNotificationCenter should be both involved. The prior one for the iOS 7+ devices and the later one for iOS 10+ devices.
Update:
For iOS 10+, you could use UNUserNotificationCenter.Current.GetDeliveredNotifications to obtain the notifications that are still displayed in Notification Center. And if you only want to support iOS version 10 and later. I think UNUserNotificationCenter is enough, there's no need to implement AppDelegate.DidReceiveRemoteNotification(...) or AppDelegate.ReceivedRemoteNotification(...).
If the app is on background / killed state and the user clicks notification to
open the app, DidReceiveNotificationResponse will be called.
If the
user clicks icon to open your app and the app is killed you should
place your logic code in FinishedLaunching.
If the user clicks icon
to open your app and app is on background, you can handle
WillEnterForeground as you did before.
If the app is on foreground,
handle WillPresentNotification.
Stack Overflow. Here is what I´m dealing with.
I´m developing an iOS 8 app that starts monitoring significant location changes when app is terminated using startMonitoringSignificantLocationChanges(). Until here everything works fine. The app relaunches when it get a location change with the application:didFinishLaunchingWithOptions:. I configure a CLLocationManager object and get the CLLocation like it says in Apple reference:
If you start this service and your app is subsequently terminated, the
system automatically relaunches the app into the background if a new
event arrives. In such a case, the options dictionary passed to the
application:willFinishLaunchingWithOptions: and
application:didFinishLaunchingWithOptions: methods of your app
delegate contains the key UIApplicationLaunchOptionsLocationKey to
indicate that your app was launched because of a location event. Upon
relaunch, you must still configure a location manager object and call
this method to continue receiving location events. When you restart
location services, the current event is delivered to your delegate
immediately. In addition, the location property of your location
manager object is populated with the most recent location object even
before you start location services.
Like I said, this works pretty well. My problem is when I try to fetch data from a web service (like Twitter API with TwitterKit) with a asynchronous method; I just not get onto the completion handler. I was researching a lot about this subject and I read that apparently the method I use is running in its own thread.
Here is how I listen to the location change launch and create the background task:
var locationManager: CLLocationManager!
var bgTaskId: UIBackgroundTaskIdentifier!
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// API keys configuration
if let options = launchOptions {
if options[UIApplicationLaunchOptionsLocationKey] != nil {
self.bgTaskId = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({ () -> Void in
UIApplication.sharedApplication().endBackgroundTask(self.bgTaskId)
self.bgTaskId = UIBackgroundTaskInvalid
})
// Location manager configuration
// Async func
theAsyncRequest(){ (response) -> Void in
//
// NEVER GETS HERE
//
UIApplication.sharedApplication().endBackgroundTask(self.bgTaskId)
self.bgTaskId = UIBackgroundTaskInvalid
}
}
return true
}
// Some code when app launches the normal way
return true
}
I tried the same with a synchronous method like in Parse Framework eg. let objects = someQuery.findObjects() and it worked. But it doesn´t with an asynchronous method.
What do I´m missing in here? Is it possible what I´m trying to accomplish? I know this would be an easy task if I´d use a synchronous method.
Thanks in advance.
I've got a problem with location services. I can't set up a function that updates my location coordinates in the background by a NSTimer. Here is my code from appDelegate:
var locationManager = CLLocationManager()
func applicationDidEnterBackground(application: UIApplication) {
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.theTimer = NSTimer(fireDate: NSDate(), interval: 40, target: self, selector: "handleTimer", userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(self.theTimer, forMode: NSDefaultRunLoopMode)
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
var locValue:CLLocationCoordinate2D = manager.location.coordinate
println("dinBack = \(locValue.latitude) \(locValue.longitude)")
self.locationManager.stopUpdatingLocation()
}
func handleTimer(){
println("started")
self.locationManager.startUpdatingLocation()
}
PS. - Of course that i've imported corelocation.
- When I get back into the app, the console prints what should have printed in the background.
You can not make an NSTimer work like this while your application is in the background. NSTimer's are not "real-time mechanisms". From the official documentation:
Timers work in conjunction with run loops. To use a timer effectively, you should be aware of how run loops operate—see NSRunLoop and Threading Programming Guide. Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.
Emphasis mine.
The important take away from this is that while your application is in the background, any run loop that your timer would have been scheduled on is not actively running.
As soon as your app returns to the foreground, this run loop fires back up, sees that your timer is overdue, and sends the message to the selector.
With iOS 7 and forward, if you want to perform operations in the background, you can tell the OS that you want to perform "background fetches".
To set this up, we must first tell the OS how frequently we want to fetch data, so in didFinishLaunching..., add the following method:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
application.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
return true
}
We can pass any time interval here (for example, if we only want to check once a day). The value we pass in is only defining a minimum amount of time that should pass between checks, however. There is no way to tell the OS a maximum amount of time between checks.
Now, we must implement the method that actually gets called when the OS gives us an opportunity to do background work:
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
// do background work
}
We can do whatever we want within this method. There are two catches, however.
This method is called while our app is in the background. The OS limits us to (I believe) thirty seconds. After thirty seconds, our time is up.
We must call the completionHandler() (or the OS will think we used all of our time).
The completionHandler that gets passed in takes an enum, UIBackgroundFetchResult. We should pass it either .Failed, .NewData, or .NoData, depending upon what our actual results were (this approach is typically used for checking a server for fresh data).
So, our method might look like this:
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
// do stuff
if let _ = error {
completionHandler(.Failed)
} else if results.count > 0 {
completionHandler(.NewData)
} else {
completionHandler(.NoData)
}
}
Keep in mind, we have absolutely zero control over how frequently the OS will actually let us run this code in the background. The OS uses several metrics in order to optimize the user's experience.
I think if your app reports .Failed to the completion handler, the OS might give you a second chance soon, however if you're abusing .Failed, the OS could probably blacklist your application from using background fetches (and Apple could deny your app).
If your app isn't reporting .NewData, the OS will let your app do background work less often. I'm not saying this because I recommend that you just always report .NewData. You should definitely report accurately. The OS is very smart about scheduling work. If you're passing .NewData when there isn't new data, the OS will let your app work more often than it may need to, which will drain the user's battery quicker (and may lead to them uninstalling your app altogether).
There are other metrics involved in when your app gets to do background work however. The OS is very unlikely to let any app do background work while the user is actively using their device, and it is more likely to let apps do background work while the user is not using their device. Additionally, OS is more likely to do background work while it is on WiFi and while it is plugged into a charger of some sort.
The OS will also look at how regularly the user uses your app, or when they regularly use it. If the user uses your app every day at 6pm, and never at any other time, it's most likely that your app will always get an opportunity to do background work between 5:30pm and 6pm (just before the user will use the app) and never during any other part of the day. If the user very rarely uses your app, it may be days, weeks, or months between opportunities to work in the background.