i'm developing a sample app that tracks the user's position in background, but i don't want to leave the location service always enabled, but something in my timer does not behave properly.
My idea was that every x minutes, the service goes on, and when it have a correct new location it is released again, now is set to 10 seconds just for testing. (Significant LocationChange did not the trick, not accurated enough)
I was searching a lot (iOS Dev center + StackOverflow) and found the "new" background location features, that allows you to run code over 10 minutes after going to background, using beginBackgroundTaskWithExpirationHandler, a few blocks, and a timer.
I set the background mode to Location and by now i think i don't need to handle the end of the background time (first i want to get a location every 15-20 seconds)
the code is working "fine" but:
The timer sometimes fires, sometimes does not.
When the timer fires, it takes a minimum of 10 minutes to do it.
Some random actions in the OS (like entering to search desktop) appears to estimulate the timer to fire (not sure of this, i don't realize how it is possible, but there it is...)
And by over the code will be another qüestion.
appdelegates's methods:
//applicationDidEnterBackground
- (void)applicationDidEnterBackground:(UIApplication *)application{
NSLog(#"to background");
UIApplication* app = [UIApplication sharedApplication];
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Do the work associated with the task.
_triggertimer = nil;
[self initTimer];
});
NSLog(#"backgroundTimeRemaining: %.0f", [[UIApplication sharedApplication] backgroundTimeRemaining]);}
//initTimer
- (void) initTimer{
NSLog(#"InitTimer ");
UIApplication *app = [UIApplication sharedApplication];
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
_triggertimer = [NSTimer scheduledTimerWithTimeInterval:10.0
target:self
selector:#selector(checkUpdates:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_triggertimer forMode:NSDefaultRunLoopMode] ;
[[NSRunLoop currentRunLoop] run];
}];}
//checkUpdates
- (void)checkUpdates:(NSTimer *)timer{
NSLog(#"CheckUpdates ");
UIApplication* app = [UIApplication sharedApplication];
if (nil == _locationManager) _locationManager = [[CLLocationManager alloc] init];
_locationManager.delegate = self;
_locationManager.distanceFilter = 10;
_locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
[_locationManager startUpdatingLocation];
[_locationManager startMonitoringSignificantLocationChanges];
double remaining = app.backgroundTimeRemaining;
NSLog(#"Reminaing %f", remaining);}
I tried lots of thing to try to fix this and maybe i messed or missed something... What do you see? maybe some concept errors, i'm trying to introduce myself to the blocks and I don't domain them yet ¬¬
By the way,why all the codes i've found contains this before doing anything with beginBackgroundTaskWithExpirationHandler?
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
I thought that this is for taking that 600 seconds of background... but i'm not sure!
When your app is backgrounded the timer will no longer fire unless you have the "location" value for the UIBackgroundModes key set in the app's info.plist.
You can extend the time that you are allowed to run in the background (if you haven't set "location") using beginBackgroundTaskWithExpirationHandler but that should always be paired with the corresponding end call.
Ok, the problem was that i was calling beginBackgroundTaskWithExpirationHandler twice, one in ApplicationDidEnterBackground and another one inside initTimer...
Related
I'm developing an application that uses a background task to keep tracking of the user position every 20 seconds. All is fine, except that when I enter the application in background, a new background tasks is created, so that I can have in final multiple background tasks that are running.
I tried to add [[UIApplication sharedApplication] endBackgroundTask:bgTask]; in applicationWillEnterForeground, but that do nothing.
The point is that I want to invalidate/disable all running background tasks when I enter the app and create a new one when I enter in background, or to keep a just one background task running.
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[self runBackgroundTask:10];
}
-(void)runBackgroundTask: (int) time{
//check if application is in background mode
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
__block UIBackgroundTaskIdentifier bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSTimer* t = [NSTimer scheduledTimerWithTimeInterval:time target:self selector:#selector(startTracking) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:t forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
});
}
}
-(void)startTracking{
//Location manager code that is running well
}
I would suggest changing UIBackgroundTaskIdentifier to be a property of the app delegate class and initialize it to UIBackgroundTaskInvalid in didFinishLaunchingWithOptions. Then, in your other app delegate methods, you can just check the value of this property to determine whether there is a background task identifier to end or not.
--
An unrelated observation, but you don't need that runloop stuff. Just schedule the timer on the main thread/runloop (with scheduledTimerWithTimeInterval) and get rid of all of that runloop stuff (because you already added it to the main runloop and that runloop is already running).
For example, let's assume I wanted to do something every 10 seconds while the app was in background, I'd do something like the following:
#interface AppDelegate ()
#property (atomic) UIBackgroundTaskIdentifier bgTask;
#property (nonatomic, weak) NSTimer *timer;
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.bgTask = UIBackgroundTaskInvalid;
return YES;
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
self.bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
if (self.bgTask != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.bgTask];
self.bgTask = UIBackgroundTaskInvalid;
}
}];
self.timer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:#selector(startTracking) userInfo:nil repeats:YES];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// invalidate the timer if still running
[self.timer invalidate];
// end the background task if still present
if (self.bgTask != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.bgTask];
self.bgTask = UIBackgroundTaskInvalid;
}
}
- (void)startTracking{
NSLog(#"%s", __PRETTY_FUNCTION__);
}
Now, in your code sample, the timer wasn't a repeating timer, so if you only wanted to fire the timer once, then set repeats to NO, but then make sure that startTracking then ended the background task (no point in keeping the app alive if you're not going to do anything else).
BTW, make sure you run this code on a device, and do not run it from Xcode (as being attached to Xcode changes the background behavior of apps).
Specify location background mode
Use an NSTimer in the background by using UIApplication:beginBackgroundTaskWithExpirationHandler:
In case n is smaller than UIApplication:backgroundTimeRemaining ,it will work just fine, in case n is larger, the location manager should be enabled (and disabled) again before there is no time remaining to avoid the background task being killed.
This does work since location is one of the three allowed types of background execution.
Note: Did loose some time by testing this in the simulator where it doesn't work, works fine on phone.
Is it possible to support background location updates from iOS6.0 all the way to the new iOS9?
Also, is is possible to keep the app updating the location in background even when the app is closed or when the device is locked?
Can you please give me some documentation about how to do that?
Thanks in advance!
Update
I've downloaded a sample project from here but for some reason it's not updating location in background, even when using simulated location and moving more than 500m. So i've changed this 2 methods:
- (void)startMonitoringLocation {
if (_anotherLocationManager)
[_anotherLocationManager stopMonitoringSignificantLocationChanges];
self.anotherLocationManager = [[CLLocationManager alloc]init];
_anotherLocationManager.delegate = self;
_anotherLocationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
_anotherLocationManager.activityType = CLActivityTypeOtherNavigation;
if(IS_OS_8_OR_LATER) {
[_anotherLocationManager requestAlwaysAuthorization];
}
[_anotherLocationManager startMonitoringSignificantLocationChanges];
}
- (void)restartMonitoringLocation {
[_anotherLocationManager stopMonitoringSignificantLocationChanges];
if (IS_OS_8_OR_LATER) {
[_anotherLocationManager requestAlwaysAuthorization];
}
[_anotherLocationManager startMonitoringSignificantLocationChanges];
}
And instead of using stopMonitoringSignificantLocationChanges and startMonitoringSignificantLocationChanges, I'm now using stopUpdatingLocation and startUpdatingLocation. The problem is that now when I'm going to background, it's starting to fetch location and never stops. Why?
yes it is possible , but why do you need to use the IOS 6 , i have no idea , by the way of you want to send location update at background this code will help you through this
- (void)applicationDidEnterBackground:(UIApplication *)application
{
bgTask = [application beginBackgroundTaskWithName:#"MyTask" expirationHandler:^{
// Clean up any unfinished task business by marking where you
// stopped or ending the task outright.
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Do the work associated with the task, preferably in chunks.
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
});
}
The app needs to register the background mode for location updates.
See the Background Execution chapter in the App Programming Guide for iOS
When my app is downloading big file and user switching to the other app, i'm running background task like this:
beginBackgroundTaskWithExpirationHandler:
and then if user opening "app switcher" by double click, screenshot of my app is completely random. Sometimes it's showing view controller that was not even open in the app.
ignoreSnapshotOnNextApplicationLaunch not helping, because it's not working at all.
Apple says: Avoid updating your windows and views here: documentation, but I'm not updating views.
I'm also running timer, to check how much background time is left, and this timer is the cause of my problems. If I'm not creating it, everything is working perfect, but I cannot save download state in Expiration handler - not enough time.
How can i avoid this weird behaviour?
-(void)appDidEnterBackground {
UIApplication *application = [UIApplication sharedApplication];
__block UIBackgroundTaskIdentifier bgTask;
bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
if(backgroundTimer == nil || ![backgroundTimer isValid]) {
backgroundTimer = [[NSTimer alloc]
initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:0]
interval:1
target:self
selector:#selector(checkBackgroundTimeRemaining)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:backgroundTimer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
}
}
- (void)checkBackgroundTimeRemaining {
NSTimeInterval timeRemaining = [[UIApplication sharedApplication] backgroundTimeRemaining];
if(timeRemaining < 5) {
if(backgroundTimer != nil) {
[backgroundTimer invalidate];
backgroundTimer = nil;
}
[downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
[self saveResumeData:resumeData];
}];
}
}
Sometimes it's showing view controller that was not even open in the app.
This sounds really fishy and should never happen. Maybe you can add some code to show what you are doing?
ignoreSnapshotOnNextApplicationLaunch is irrelevant here since it's only used to determine what happens when the user taps on your icon again to open the app.
Did you maybe forget to call endBackgroundTask: when you've finished your background task?
I'm not sure what you intend with the timer? If it is to determine how much time is left for you to execute in the background, use UIApplication's backgroundTimeRemaining instead.
I have tried this but not working more than 180 sec in iOS 7 and Xcode 4.6.2. Please help me
UIBackgroundTaskIdentifier bgTask = UIBackgroundTaskInvalid;
UIApplication *app = [UIApplication sharedApplication];
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:bgTask];
}];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:20 target:self selector:#selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-(void) timerMethod{
NSLog(#"in timerMethod");
}
Unless you enable one of the Background modes, it is not gonna work.
Why?
You have around 10 minutes of background execution after this the timer is stopped by ios.
The timer will not fire after app is locked (iOS7), since ios suspends the foreground app and bgTask will not get fire again.
There is some workarounds, consider to check below question:
iphone - NSTimers in background
Scheduled NSTimer when app is in background?
NSTimer on background IS working
I would like to trigger action after some ammount of time (in production that will be 30 minutes) and right now I'm using NSTimers scheduledTimerWithTimeInterval. During tests (with timeout being 20 seconds, not 1800 seconds) everything seems fine. In debbuging mode (running from XCode) everything seems fine as well, because the device does not autolock. But in real life, when application is ran on device, autolocking (precisely autolocking, triggering lock button does not) "freezes" the timer (or at least moves the timer trigger somehow to future).
Can I handle that situation? Of course I can disable idleTimer in UIApplication sharedApplication but when application will enter background mode ipad still can autolock.
Actually you can,
First of all, create a default value for "startTimeOfMyExoticTimer" to NSUserDefaults:
[[[NSUserDefaults] standardDefaults] setObject:[NSDate date] forKey:#"startTimeOfMyExoticTimer"];
Then kick of a timer to check if the valid time range is over:
[NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:#selector(checkIfTimeIsOver) userInfo:nil repeats:YES];
Then implement your checker method:
-(void)checkIfTimeIsOver{
NSDate * now = [NSDate date];
NSDate * startTime = [[NSUserDefaults standardDefaults] objectForKey:#"startTimeOfMyExoticTimer"];
// Here compare your now and startTime objects. (subsract them) and see the difference between them. If your time range is to be set on 30 seconds. Then you should check if the time difference between those objects are bigger than 30 seconds.
}
This will be working even if the device is locked, the app is in background etc.
This approach worked for me, hope it'll work for you too
//Implement this block help your application run background about 10 minutes
#interface AppDeledate(){
UIBackgroundTaskIdentifier backgroundTaskId;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// if missing interval exist > start timer again and set missing interval to 0
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
NSLog(#"Did Enter Background");
backgroundTaskId = [application beginBackgroundTaskWithExpirationHandler:^{
NSLog(#"Background Task Expired !!!\n");
[application endBackgroundTask:backgroundTaskId];
backgroundTaskId = UIBackgroundTaskInvalid;
//save missing interval to NSUserDefault
}];
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
NSLog(#"Will Enter Foreground");
if (backgroundTaskId && backgroundTaskId != UIBackgroundTaskInvalid) {
[application endBackgroundTask:backgroundTaskId];
backgroundTaskId = UIBackgroundTaskInvalid;
}
}