I'm building an app with Region monitoring. It works fine in foreground but once the app is sent in background, it's not behaving as expected: it does call didEnter and didExit but as soon as it starts executing the callbacks it stops. In these callback methods i need to poll a server and persist didExitRegion and/or didEnterRegion status. As soon as I put the app in foreground again, any queued request starts and completes.
Any idea?
I'm using ios5.1 and ios6 on iphone 4
When you get called in the background in
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
(or ...exit)
just setup whatever you need for the server call (variables, payload for the server etc etc).
Before the actual sending start a
self.bgTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:self.bgTaskId];
self.bgTaskId = UIBackgroundTaskInvalid;
somelogger(#"ran out of time for background task");
// remember that we failed for the next time the app comes to the foreground
}];
Then do the actual sending with the HTTP framework of your choice and in the completion handlers reset the background task with
[[UIApplication sharedApplication] endBackgroundTask:self.bgTaskId];
Example using AFNetworking:
[self.httpClient postPath:#"state" parameters:#{#"abc": abc, #"value": val, #"h": h, #"app":myAppName , #"version": myAppVersion }
success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (operation.response.statusCode != 200) {
DDLogVerbose(#"response was not 200. error: %i", operation.response.statusCode);
} else {
DDLogVerbose(#"success");
}
[[UIApplication sharedApplication] endBackgroundTask:self.bgTaskId];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
DDLogVerbose(#"request error %#, current retry count %d", error, retryCount );
// start our own retry mechanism
if (retryCount < MAX_RETRIES) {
retryCount++;
double delayInSeconds = RETRY_INTERVAL * (1 + (double)retryCount/10);
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// try again
});
} else {
// final
[[UIApplication sharedApplication] endBackgroundTask:self.bgTaskId];
// remember failure when app comes back to foreground
}
}];
I am using a
#property (assign, nonatomic) UIBackgroundTaskIdentifier bgTaskId;
to store the background identifier.
The whole mechanism is explained in http://developer.apple.com/library/ios/#documentation/iphone/conceptual/iphoneosprogrammingguide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html#//apple_ref/doc/uid/TP40007072-CH4-SW28
you have to request additional time if you want to stay alive.
see applle docu on background mode. There is a method for that.
generally you are not "allowed" to stay active in background for any task. only for specific ones, like GPS.
try to request additional background time after each region update.
If you havent already add 'location' to the UIBackgroundModes of your Info.plist. As a second idea I'd use AFNetworking which is widely popular and has backgrounding support. That means it will deal with setting up the parameters to tell the OS that it will "finish this thing before I go back to sleep".
Related
My Apple Watch app sends a message to the companion iPhone app. In the main app's handleWatchKitExtensionRequest, I send a request to the server:
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply {
if ([[userInfo objectForKey:#"request"] isEqualToString:#"getPendingChallenge"]) {
[MyClient getPendingNotifications:someId withDomain:host withSuccessBlock:^(id responseObject) {
// process responseObject
...
reply(response);
return;
} withFailureBlock:^(NSError *error, NSString *responseString) {
// error handling
return;
}];
}
}
getPendingNotifications above is just a regular network GET request using AFNetworking.
It all works well when the app is active. Because this network request is used to populate the UI on my Apple Watch, I do not wish the main app to be active. However, when the main app on iPhone is in background, I can see the network request being sent out, but the withSuccessBlock or withFailureBlock callback blocks in the above code never gets triggered.
Can the phone app receive network request responses in background mode? If so, what am I doing wrong?
I have found a solution online that works for me, a post (http://www.fiveminutewatchkit.com/blog/2015/3/11/one-weird-trick-to-fix-openparentapplicationreply) by Brian Gilham.
And here's the code that works for me.
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply {
// There is a chance that the iOS app gets killed if it's in the background
// before it has a chance to reply to Apple Watch.
// The solution is to have the app respond to the request asap, then complete other tasks.
// The following code begins – and ends, after two seconds – an empty background task right at the beginning of this delegate method
// Then we kick off a background task for the real work
// For more details see http://www.fiveminutewatchkit.com/blog/2015/3/11/one-weird-trick-to-fix-openparentapplicationreply
__block UIBackgroundTaskIdentifier bogusWorkaroundTask;
bogusWorkaroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:bogusWorkaroundTask];
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] endBackgroundTask:bogusWorkaroundTask];
});
__block UIBackgroundTaskIdentifier realBackgroundTask;
realBackgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
reply(nil);
[[UIApplication sharedApplication] endBackgroundTask:realBackgroundTask];
}];
if ([[userInfo objectForKey:#"request"] isEqualToString:#"getPendingChallenge"]) {
[self handleWatchKitGetPendingChallengeRequest:reply];
}
[[UIApplication sharedApplication] endBackgroundTask:realBackgroundTask];
}
- (void)handleWatchKitGetPendingChallengeRequest:(void (^)(NSDictionary *))reply {
...
[MyClient getPendingNotifications:someId withDomain:host withSuccessBlock:^(id responseObject) {
// process responseObject
reply(response);
return;
} withFailureBlock:^(NSError *error, NSString *responseString) {
// error handling
reply(nil);
return;
}];
}
Try to send the request as a synchronous request.
I guess that your request is asynchronous request (as it should be in regular cases). The problem that in background mode, the device will lunch your app in background thread, and you created a new thread for the request.
Can I write the code in this method to get the M7's data and does it useful while I don't run the app?
-(void)locationManager:(CLLocationManager *)manager didVisit:(CLVisit*)visit
{
}
Yes, you can query CMMotionActivityManager from locationManager:didVisit:
Please note that visits are not reported to your app in real time, in my tests they are be delayed 20 to 60 minutes. That means starting activity monitoring with startActivityUpdatesToQueue:withHandler: makes no sense, as these updates won't tell you what happened during the visit.
However you still can fetch and analyze activity events that happened during the visit using queryActivityStartingFromDate:toDate:toQueue:withHandler:
Keep in mind that locationManager:didVisit: might and most likely will be called while your app is in background mode, thus you have about 10 seconds to query the CMMotionActivityManager and process the data. Since you have no control over the CMMotionActivityManager and there is no guarantee it will process your query in a timely fashion, you may want to invoke beginBackgroundTaskWithExpirationHandler: in your locationManager:didVisit: as well.
#property (nonatomic) UIBackgroundTaskIdentifier bgTask;
#property (nonatomic, strong) CMMotionActivityManager *motionActivityManager;
...
- (void)locationManager:(CLLocationManager *)manager didVisit:(CLVisit *)visit
{
if (visit.arrivalDate && visit.departureDate) {
// use strong self here, as we must end the background task explicitly
self.bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^ {
[self stopBackgroundTask];
}];
[self.motionActivityManager queryActivityStartingFromDate:visit.arrivalDate
toDate:visit.departureDate
toQueue:[NSOperationQueue currentQueue]
withHandler:^(NSArray *activities, NSError *error) {
// handle CMMotionActivity history here
[self stopBackgroundTask];
}];
}
}
- (void) stopBackgroundTask {
if (self.bgTask != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.bgTask];
self.bgTask = UIBackgroundTaskInvalid;
}
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[_dataStore saveChanges];
[_sync syncWithServerWithDate:[NSDate dateWithTimeIntervalSince1970:timestamp]];
}
-(void)syncWithServerWithDate:(NSDate *)date
{
void(^postCompletionBlock)(FTjsonEvents *obj, NSError *error) = ^(FTjsonEvents *serverEvents, NSError *error) {
...
NSLog(#"Post Completion block finished!");
};
void(^completionBlock)(FTjsonEvents *obj, NSError *error) = ^(FTjsonEvents *serverEvents, NSError *error) {
....
NSLog(#"Fetch finished!");
[self postRecordsSinceLastServerSyncTimestamp:[date timeIntervalSince1970] WithCompletion:postCompletionBlock];
};
NSLog(#"Syncing data...");
[self fetchRecordsByDate:date WithCompletion:completionBlock];
}
I would like to sync with the server to fetch and post the latest data to.
Since this is happening via async completion blocks, it seems that my classes get garbage collected once I press home button. The sync never reaches the server.
However the local coredata is easily saved when doing this: [_dataStore saveChanges];
Is there a way to keep the async sync alive in the background until its completed?
Your implementation of applicationDidEnterBackground: has approximately five seconds to perform any tasks and return. If you need additional time to perform any final tasks, you can request additional execution time from the system by calling beginBackgroundTaskWithExpirationHandler:. In practice, you should return from applicationDidEnterBackground: as quickly as possible. If the method does not return before time runs out your app is terminated and purged from memory.
You should perform any tasks relating to adjusting your user interface before this method exits but other tasks (such as saving state) should be moved to a concurrent dispatch queue or secondary thread as needed. Because it's likely any background tasks you start in applicationDidEnterBackground: will not run until after that method exits, you should request additional background execution time before starting those tasks. In other words, first call beginBackgroundTaskWithExpirationHandler: and then run the task on a dispatch queue or secondary thread.
Here's an example implementation:
#interface XXAppDelegate (BackgroundStuff)
#property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTask;
#end
#implementation
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Request additional background time.
self.backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:self.backgroundTask];
}];
// Start background task.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Background code goes here
//Cleanup background task id
[application endBackgroundTask:self.backgroundTask];
self.backgroundTask = UIBackgroundTaskInvalid;
});
}
#end
I'm starting a background task in -applicationDidEnterBackground that uploads data to my server, if the user has changed settings relevant to the push notifications. When the user changes a setting I set a static BOOL to YES and only send the changes when the app enters the background. I pass the a block ending the task to the method so when reaching connectionDidFinishLoading it calls it and ends the task.
It works most the times on the simulator, but doesn't work on the actual device.
Relevant code:
self.bgTask = [application beginBackgroundTaskWithExpirationHandler:^
{
[application endBackgroundTask:self.bgTask];
self.bgTask = UIBackgroundTaskInvalid;
}];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
{
[PushInfo checkDirty:^{
NSLog(#"push info sent");
[application endBackgroundTask:self.bgTask];
self.bgTask = UIBackgroundTaskInvalid;
}];
});
...
// in PushInfo.m :
typedef void (^VoidBlock)();
static BOOL dirty;
+ (void) checkDirty:(VoidBlock)endBlock
{
if(dirty)
{
PushInfo *pi = [[PushInfo alloc] init];
NSLog(#"sending pushinfo"); // This code is always reached
[pi setEndBlock:endBlock];
[pi updatePushInfo];
}
else
endBlock();
}
- (void) updatePushInfo
{
...
// Create a NSURLConnection to send the data
...
}
- (void) connectionDidFinishLoading:(NSURLConnection *)connection
{
...
NSLog(#"sent push info");
dirty = NO;
if(endBlock)
{
endBlock();
}
}
Am I missing anything ?
EDIT : even when it does send the information to the server on the simulator, the static variable is still YES for some reason...
Try moving your code to:
-(void) applicationWillResignActive:(UIApplication *)application
I believe having it in applicationDidEnterBackground is too late.
Checked the docs, and you are right, it should be fine in applicationDidEnterBackground.
Another suggestion, try moving beginBackgroundTaskWithExpirationHandler inside the dispatch_async block. It may have to be started in the same thread but didn't see this explicitly stated in the documentation.
ended using +sendSynchronousRequest:returningResponse:error:
My app got rejected by Apple three times, all with the same rejection letter, which is:
We found that your app uses a background mode but does not include
functionality that requires that mode to run persistently. This
behavior is not in compliance with the App Store Review Guidelines.
We noticed your app declares support for location in the
UIBackgroundModes key in your Info.plist but does not include features
that require persistent location.
It would be appropriate to add features that require location updates
while the app is in the background or remove the "location" setting
from the UIBackgroundModes key.
If you choose to add features that use the Location Background Mode,
please include the following battery use disclaimer in your
Application Description:
"Continued use of GPS running in the background can dramatically
decrease battery life."
For information on background modes, please refer to the section
"Executing Code in the Background" in the iOS Reference Library.
Now, as far as I know I am running on the background and "doing something"...
In my AppDelegate I have the following code in didFinishLaunchingWithOptions:
if ([[launchOptions allKeys] containsObject:UIApplicationLaunchOptionsLocationKey] &&
([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]))
{
id locationInBackground = [launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey];
if ([locationInBackground isKindOfClass:[CLLocation class]])
{
[self updateMyLocationToServer:locationInBackground];
}
else
{
//Keep updating location if significant changes
CLLocationManager *locationManager = [[CLLocationManager alloc] init];
self.bgLocationManager = locationManager;
self.bgLocationManager.delegate = self;
self.bgLocationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
[bgLocationManager startMonitoringSignificantLocationChanges];
}
}
The AppDelegate also starts a location manager and makes himself the delegate.
Then, I have the following code for handling the location updates on the background:
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
[self updateMyLocationToServer:newLocation];
}
-(void)updateMyLocationToServer:(CLLocation*)myNewLocation
{
// NSLog(#"Updating Location from the background");
NSString *fbID = [NSString stringWithString:[facebookDetails objectForKey:#"fbID"]];
NSString *firstName = [NSString stringWithString:[facebookDetails objectForKey:#"firstName"]];
NSString *lastName = [NSString stringWithString:[facebookDetails objectForKey:#"lastName"]];
NSString *urlString = [NSString stringWithFormat:#"MY_SERVER_API", fbID, myNewLocation.coordinate.latitude, myNewLocation.coordinate.longitude, firstName, lastName];
NSURL *url = [NSURL URLWithString:urlString];
__block ASIHTTPRequest *newRequest = [ASIHTTPRequest requestWithURL:url];
[newRequest setCompletionBlock:^{
}];
[newRequest setFailedBlock:^{
}];
// [newRequest setDelegate:self];
[newRequest startAsynchronous];
}
I also put a disclaimer in my app description page:
Intensive use of GPS running in the background can dramatically decrease battery life. For this reason, MY_APP_NAME runs on the background just listening for significant location changes.
Is there anything I'm missing here?
This question is old and already answered but you dont need the UIBackgroundModes key if you collect locations using the startMonitoringSignificantLocationChanges API
In locationManager:didUpdateToLocation:fromLocation: or in updateMyLocationToServer: You should check if application is in background state by eg.
[UIApplication sharedApplication].applicationState == UIApplicationStateBackground
And then if Your app is in background mode You should use eg.
backgroundTask = [[UIApplication sharedApplication]
beginBackgroundTaskWithExpirationHandler:
^{
[[UIApplication sharedApplication] endBackgroundTask:backgroundTask];
}];
/*Write Your internet request code here*/
if (bgTask != UIBackgroundTaskInvalid)
{
[[UIApplication sharedApplication] endBackgroundTask:backgroundTask];
bgTask = UIBackgroundTaskInvalid;
}
This way application should perform this task completely.
startMonitoringSignificantLocationChanges don't require a background mode registration. Only continuous location changes do.