I am working on an app that needs to regularly download data, when the app is in the foreground or background.
I have set up networking using NSURLSessionDownloadTask, such that it works fine in all situations but one.
When the app is running on a physical device, in the foreground and networking is on mobile data, the task resumes to running state, but never downloads any data, never gets any progress and never suspends.
It works fine if I switch on wifi, run from Xcode (debug or release) or run on a simulator.
I should also mention that it is intermittent; I could reproduce it yesterday, but not today, so it is possible that background networking is affected.
Edit - Example code
Due to the complexity of the application and the limited time I can give to this, I can't give a whole application, but I have included the relevant methods from the AppDelegate and JobsManager classes and the whole of the BackgroundJobFetcher class.
AppDelegate methods:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// other stuff
[BackgroundJobFetcher sharedInstance];
}
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
[[BackgroundJobFetcher sharedInstance] setFetchCompletionHandler:completionHandler];
[[JobsManager sharedInstance] fetchAllJobs];
}
- (void) application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {
[[BackgroundJobFetcher sharedInstance] setSavedCompletionHandler:completionHandler];
}
JobsManager methods:
- (void) initiateProcesses {
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:30 repeats:YES block:^(NSTimer * _Nonnull timer) {
[self fetchAllJobs];
}];
}
- (void) fetchAllJobs {
DefaultsManager *dManager = [DefaultsManager sharedDefaultsManager];
NSString *apiFunction = @"getAllJobs";
NSArray* optionsList = @[dManager.CustomerID, dManager.UserID];
[self callAPIFunction:apiFunction options:optionsList];
}
- (void) callAPIFunction:(NSString*)apiFunction options:(NSArray*)options {
NSString *apiBaseURL = @"https://www.*****.com/rest";
NSString *urlComplete = [NSString stringWithFormat:@"%@/%@",
apiBaseURL,
apiFunction];
for (NSString* option in options) {
urlComplete = [NSString stringWithFormat:@"%@/%@", urlComplete, option];
}
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlComplete]
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:30.0];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[BackgroundJobFetcher sharedInstance] handleRequest:request];
});
}
BackgroundJobFetcher.h:
#import <Foundation/Foundation.h>
@interface BackgroundJobFetcher : NSObject
@property (nonatomic, copy) void (^ _Nullable savedCompletionHandler)();
@property (nonatomic, copy) void (^ _Nullable fetchCompletionHandler)(UIBackgroundFetchResult);
+ (BackgroundJobFetcher*_Nonnull)sharedInstance;
- (void) handleRequest:(NSURLRequest*_Nonnull)request;
- (void) getAllTasksWithCompletionHandler:(void(^_Nonnull)(NSArray<__kindof NSURLSessionTask *> * _Nonnull tasks))completionHandler;
- (void) stopAllTasks;
@end
BackgroundJobFetcher.m:
#import "BackgroundJobFetcher.h"
#import "JobsManager.h"
#import "DefaultsManager.h"
#import "AppDelegate.h"
@interface BackgroundJobFetcher() <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate>
@property (nonatomic, strong) NSMutableData* responseData;
@property (nonatomic, retain) NSURLSession *defaultSession;
@end
@implementation BackgroundJobFetcher
+ (BackgroundJobFetcher*)sharedInstance {
static BackgroundJobFetcher* _sharedInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_sharedInstance = [BackgroundJobFetcher new];
NSURLSessionConfiguration* sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"MonitorJobFetcher"];
sessionConfiguration.sessionSendsLaunchEvents = YES;
sessionConfiguration.discretionary = YES;
_sharedInstance.defaultSession = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:_sharedInstance
delegateQueue:nil];
});
return _sharedInstance;
}
- (void) handleRequest:(NSURLRequest*)request {
NSURLSessionDownloadTask* downloadTask = [self.defaultSession downloadTaskWithRequest:request];
[downloadTask resume];
}
- (void) getAllTasksWithCompletionHandler:(void(^)(NSArray<__kindof NSURLSessionTask *> * _Nonnull tasks))completionHandler {
[self.defaultSession getAllTasksWithCompletionHandler:completionHandler];
}
- (void) stopAllTasks {
[self.defaultSession getAllTasksWithCompletionHandler:^(NSArray<__kindof NSURLSessionTask *> * _Nonnull tasks) {
for (NSURLSessionTask* task in tasks) {
[task cancel];
}
}];
}
#pragma mark NSURLSessionDelegate methods
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
DLog(@"%@", error.localizedDescription);
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
if (self.savedCompletionHandler) {
self.savedCompletionHandler();
self.savedCompletionHandler = nil;
}
}
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
DefaultsManager* dManager = [DefaultsManager sharedDefaultsManager];
NSURLCredential *credential = [NSURLCredential credentialWithUser:dManager.Email
password:dManager.Password
persistence:NSURLCredentialPersistenceForSession];
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, credential);
}
#pragma mark NSURLSessionTaskDelegate methods
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
if (!self.responseData) {
self.responseData = [NSMutableData dataWithData:data];
} else {
[self.responseData appendData:data];
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSString* urlString = task.originalRequest.URL.absoluteString;
if (error) {
DLog(@"error: %@", error.localizedDescription);
[[JobsManager sharedInstance] requestFailedWithError:error fromURL:urlString];
} else {
[[JobsManager sharedInstance] jobsFetched:self.responseData];
}
self.responseData = nil;
if (self.fetchCompletionHandler) {
self.fetchCompletionHandler(UIBackgroundFetchResultNewData);
self.fetchCompletionHandler = nil;
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
DefaultsManager* dManager = [DefaultsManager sharedDefaultsManager];
NSURLCredential *credential = [NSURLCredential credentialWithUser:dManager.Email
password:dManager.Password
persistence:NSURLCredentialPersistenceForSession];
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, credential);
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler {
completionHandler(request);
}
- (void)URLSession:(NSURLSession *)session taskIsWaitingForConnectivity:(NSURLSessionTask *)task {
DLog(@"URL: %@", task.originalRequest.URL.absoluteString);
DLog(@"task.taskIdentifier: %lu", (unsigned long)task.taskIdentifier);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
{
[downloadTask resume];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
DLog(@"URL: %@", task.originalRequest.URL.absoluteString);
DLog(@"task.taskIdentifier: %lu", (unsigned long)task.taskIdentifier);
DLog(@"metrics: %@", metrics);
}
#pragma mark NSURLSessionDownloadDelegate methods
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
self.responseData = [[NSData dataWithContentsOfURL:location] mutableCopy];
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
{
DLog(@"downloadTask.taskIdentifier: %lu", (unsigned long)downloadTask.taskIdentifier);
DLog(@"fileOffset: %lld", fileOffset);
DLog(@"expectedTotalBytes: %lld", expectedTotalBytes);
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
DLog(@"downloadTask.taskIdentifier: %lu", (unsigned long)downloadTask.taskIdentifier);
DLog(@"bytesWritten: %lld", bytesWritten);
DLog(@"totalBytesWritten: %lld", totalBytesWritten);
}
@end