I'm trying to implement background fetch in an iOS app. In the official documentation is stated:
"You don’t have to do all background network activity with background sessions ... ... . Apps that declare appropriate background modes can use default URL sessions and data tasks, just as if they were in the foreground."
Moreover, in this WWDC 2014 video at 51:50 between the best practices is stated:
"One common mistake that we think some people have made in the past is assuming that when running in the background to handle things like a background fetch update... ...they have been required to use background session. Now, using background upload or download... ...works really well, but in particular for large downloads. When you're running for a background fetch update ... you have about 30 to 60 seconds to run in the background" [before the application gets suspended] "If you have same small networking task that could finish within this time it is perfectly ok to do this in an in-process or default NSURLSession ... ..."
I am exactly in the situation of a small networking task: I want to communicate with a rest api on our server, to post same data and get back the data response. I'm creating a single default session and executing one or two tasks, synchronising them using a dispatch group, like in this example:
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
NSLog(@"AppDelegate application:performFetchWithCompletionHandler:");
BOOL thereIsALoggedInUser = [self thereIsALoggedInUser];
if (!(thereIsALoggedInUser && [application isProtectedDataAvailable])){
completionHandler(UIBackgroundFetchResultNoData);
NSLog(@"completionHandler(UIBackgroundFetchResultNoData): thereIsALoggedInUser: %@, isProtectedDataAvailable: %@",
thereIsALoggedInUser ? @"YES" : @"NO",
[application isProtectedDataAvailable] ? @"YES" : @"NO");
return;
}
NSArray<NSString*> *objectsIWantToSend = [self getObjectsIWantToSend];
if (!objectsIWantToSend) {
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
completionHandler(UIBackgroundFetchResultNoData);
NSLog(@"No post data");
} else {
[self sendToServer:objectsIWantToSend usingCompletionHandler:completionHandler];
}
}
-(void) sendToServer:(NSArray<NSString*> *)objectsArray usingCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
NSLog(@"AppDelegate - sendToServer:usingCompletionHandler:");
__block BOOL thereWasAnError = NO;
__block BOOL thereWasAnUnsuccessfullHttpStatusCode = NO;
dispatch_group_t sendTransactionsGroup = dispatch_group_create();
NSURLSession *postSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
for (NSString *objectToPost in objectsArray) {
dispatch_group_enter(sendTransactionsGroup);
NSMutableURLRequest *request = [NSMutableURLRequest new];
[request setURL:[NSURL URLWithString:@"restApiUrl"]];
[request setHTTPMethod:@"POST"];
request = [Util setStandardContenttypeAuthorizationAndAcceptlanguageOnRequest:request];
[request setHTTPBody:[NSJSONSerialization dataWithJSONObject:@{ @"appData": objectToPost } options:kNilOptions error:nil]];
NSURLSessionDataTask *dataTask = [postSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
NSInteger statusCode = -1;
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
statusCode = [(NSHTTPURLResponse *)response statusCode];
}
if (error) {
NSLog(@"Error");
thereWasAnError = YES;
} else if (statusCode != 201) {
NSLog(@"Error: http status code: %d", httpResponse.statusCode);
thereWasAnUnsuccessfullHttpStatusCode = YES;
} else {
[self parseAndStoreData:data];
}
dispatch_group_leave(sendTransactionsGroup);
}];
[dataTask resume];
}
dispatch_group_notify(sendTransactionsGroup, dispatch_get_main_queue(),^{
if (thereWasAnError) {
completionHandler(UIBackgroundFetchResultFailed);
} else if (thereWasAnUnsuccessfullHttpStatusCode) {
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
completionHandler(UIBackgroundFetchResultNoData);
} else {
completionHandler(UIBackgroundFetchResultNewData);
}
});
}
In my application:didFinishLaunchingWithOptions: method I am setting the BackgroundFetchInterval to the MinimumInterval using
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
I am not force-quitting the app on the device because I know that the system in that case won't call the AppDelegate application:performFetchWithCompletionHandler: method until a user restarts the app
I'm using several test cases with several combinations of devices locked/unlocked, charging/not charging, all under a wifi network because they've got no sims
When I run the aforementioned kind of code using Xcode -> Debug -> Simulate background fetch or using a build scheme with the "launch due to a background fetch event" option selected all goes well: the server gets my post request, the app correctly has the server response back, and the dispatch_group_notify block gets executed. Testing the code through an actual device I get instead no result, not even the call to the server
Either background fetch is not being initiated or something is going wrong during the background fetch. We need to do a little debugging to identify what the issue is.
So, a couple of observations:
Am I correct in assuming that you’re calling this from your performFetchWithCompletionHandler
method of your app delegate, passing the completion handler parameter? Perhaps you can share your implementation of this method.
It’s extremely useful to be able to monitor your app while it’s running on a physical device, but not attached to the Xcode debugger (because the fact that it’s run from the debugger changes the app lifecycle).
To this end, I’d suggest using Unified Logging instead of NSLog
, that way you can easily monitor your device os_log
statements from the macOS Console even though you’re not connected to Xcode (unlike running from Xcode, the unified logging does not affect the app lifecycle). Then, watching in macOS Console, you can confirm the initiation of the background fetch, and see if and where it failed. See WWDC video Unified Logging and Activity Tracing.
So, import os.log
:
@import os.log;
Define a variable for your log
:
os_log_t log;
Set it:
NSString *subsystem = [[NSBundle mainBundle] bundleIdentifier];
log = os_log_create([subsystem cStringUsingEncoding:NSUTF8StringEncoding], "background.fetch");
Then you can now log messages, e.g.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
os_log(log, "%{public}s", __FUNCTION__);
return YES;
}
And then use it go to your console, include info and debugging messages, and watch your device log statements appear on your macOS console, even when not running the app through the Xcode debugger, e.g.:
See that video for more information.
Anyway, armed with this logging, you can determine whether the problem is that background fetch isn’t being initiated or something in your routine.
Also, I assume you are not force-quitting the app on the device? I.e. you should just run the app on the device, and then return to the home screen and or go to another app. If you force-quit an app, that can stop some of these background operations from taking place at all.
How long have you waited for the background fetch to be initiated? Is the device in a state to allow the background fetch to be called in a timely manner?
Bottom line, you have no control when this takes place. For example, make sure you’re connected to power and are on wifi to give it the best chance to fire quickly. (The OS considers these factors when deciding when to fetch.) Last time I checked this pattern, it took about 10 minutes for the first background fetch to occur in this optimal situation.
As an aside, unrelated to your question at hand, you really should check to make sure that the response
was a NSHTTPURLResponse
. In the unlikely event it wasn’t, do you want your app to crash when you try to retrieve the statusCode
? Thus:
os_log(log, "%{public}s", __FUNCTION__);
__block BOOL thereWasAnError = NO;
__block BOOL thereWasAnUnsuccessfullHttpStatusCode = NO;
dispatch_group_t sendTransactionsGroup = dispatch_group_create();
NSURLSession *postSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
for (NSString *objectToPost in objectArray) {
dispatch_group_enter(sendTransactionsGroup);
NSMutableURLRequest *request = [NSMutableURLRequest new];
[request setURL:[NSURL URLWithString:@"restApiUrl"]];
[request setHTTPMethod:@"POST"];
request = [Util setStandardContenttypeAuthorizationAndAcceptlanguageOnRequest:request];
[request setHTTPBody:[NSJSONSerialization dataWithJSONObject:@{ @"appData": objectToPost } options:kNilOptions error:nil]];
NSURLSessionDataTask *dataTask = [postSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
NSInteger statusCode = -1;
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
statusCode = [(NSHTTPURLResponse *)response statusCode];
}
if (error) {
os_log(log, "%{public}s: Error: %{public}@", __FUNCTION__, error);
thereWasAnError = YES;
} else if (statusCode < 200 || statusCode > 299) {
os_log(log, "%{public}s: Status code: %ld", __FUNCTION__, statusCode);
thereWasAnUnsuccessfullHttpStatusCode = YES;
} else {
[self parseAndStoreData:data];
}
dispatch_group_leave(sendTransactionsGroup);
}];
[dataTask resume];
}
dispatch_group_notify(sendTransactionsGroup, dispatch_get_main_queue(),^{
if (thereWasAnError) {
completionHandler(UIBackgroundFetchResultFailed);
} else if (thereWasAnUnsuccessfullHttpStatusCode) {
// [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
completionHandler(UIBackgroundFetchResultNoData);
return;
} else {
completionHandler(UIBackgroundFetchResultNewData);
}
});
I personally also wouldn’t bother changing the fetch interval on failure, though that’s up to you. The OS will consider that, itself, when determining when to next initiate background fetch to your app.