NSURLSessionConfiguration background task on cellular Network not downloading - ios

I am downloading a file from the web. File size is big some times may reach up to 100MBs some times, I want to continue downloading while to app goes to background or when the device is locked. For this i am using AFNetworking 3.0
[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:myIdentifier];
It works fine as long as i am on WiFi. When i turn off WiFi and turn on my cellular network which is 4G, it stops responding and i get no data as a result of my download request. If i use
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
every thing was fine except my download will not continue when the app goes to background.
I have also checked allowsCellularAccess on NSURLSessionConfiguration and NSURLRequest and object which is YES, but my download does not work when on cellular network.
Here is my full code
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:myIdentifier];
configuration.discretionary = YES;
configuration.sessionSendsLaunchEvents = YES;
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
NSURL *URL = [NSURL URLWithString:downloadUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSLog(#"Allow Cellular Network : %d",request.allowsCellularAccess);
NSLog(#"Allow Cellular Network for session: %d",configuration.allowsCellularAccess);
NSLog(#"Resource timneout interval: %f",configuration.timeoutIntervalForResource);
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
dispatch_async(dispatch_get_main_queue(), ^{
[self callProgressBlocksForUrl:lesson.archiveUrl withProgress:downloadProgress.fractionCompleted];
});
} destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
NSLog(#"Getting Path for File saving");
return [NSURL fileURLWithPath:fullPath];
} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
NSHTTPURLResponse * myresponse = (NSHTTPURLResponse *)response;
NSLog(#"Video downloaded, headers: %#", [myresponse.allHeaderFields description]);
}];

You should not be setting the discretionary flag. That tells the OS to wait to download the data until a convenient time (which, IIRC, basically means when the device is A. asleep, B. on power, and C. connected to Wi-Fi).

I guess discretionary flag might create the problem.
As said by apple in documentation that discretionary flag allow the download when device have convenient time and convenient resources.

Related

AFNetworking downloaded file missing

I'm using AFNetworking 3.0 to download a file, and it seems to be doing the downloading part fine, but I can't find the file afterwards.
I'm using the code below. In the download task, if I set breakpoints in the destination block, it seems as though the target path and download destination path are correct, and in fact at the point the targetPath points to a tmp file in the tmp folder which exists and contains the correctly downloaded data. However if I then hit a breakpoint in the completion handler block, the tmp file has disappeared and there is no file where my download destination path pointed.
Am I missing a step? Do I have to move this file myself, or is that something AFNetworking should be taking care of?
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
self.theRequest = [[AFHTTPRequestSerializer serializer]
requestWithMethod:self.RequestMethod //#"POST"
URLString:requestURL.absoluteString //url to my API
parameters:self.Parameters //params being sent to API
error:nil];
//headers in this example:
//"Content-Type" = "application/json"
//"X-Requested-With" = XMLHttpRequest
//token = "<API TOKEN>";
for (id key in headers) {
[self.theRequest setValue:headers[key] forHTTPHeaderField:key];
}
self.theRequest.timeoutInterval = 60 * 100;
NSURLSessionDownloadTask * downloadTask =
[manager downloadTaskWithRequest:self.theRequest
progress:^(NSProgress * _Nonnull downloadProgress) {
if(self.DownloadProgressHandler)
self.DownloadProgressHandler(downloadProgress.fractionCompleted);
}
destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
NSURL *url = [NSURL URLWithString:self.downloadDestinationPath];
NSLog(#"%#",[targetPath absoluteString]);
NSLog(#"%#",[url absoluteString]);
return url;
}
completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
[self RequestCompleteWithResponse:response responseObject:[[filePath absoluteString] dataUsingEncoding:NSUTF8StringEncoding] error:error];
}];
self.theTask = downloadTask;
[self.theTask resume];
Output from the NSLogs above:
2016-03-04 13:43:44.412 Marq[27505:154492] __23-[MarqAPI BuildRequest]_block_invoke247 line 648 $ file:///Users/aerion/Library/Developer/CoreSimulator/Devices/11594D0A-882C-4E46-9BAC-CEF7148014C7/data/Containers/Data/Application/E8C7D3EE-BB69-461F-BA2F-49EB7C2AE1CF/tmp/CFNetworkDownload_7VGArX.tmp
2016-03-04 13:43:44.425 Marq[27505:154492] __23-[MarqAPI BuildRequest]_block_invoke247 line 649 $ /Users/aerion/Library/Developer/CoreSimulator/Devices/11594D0A-882C-4E46-9BAC-CEF7148014C7/data/Containers/Data/Application/E8C7D3EE-BB69-461F-BA2F-49EB7C2AE1CF/Documents/9dfd86c2-458e-4725-a184-5fcd87f94dbd.inspect
Argh, that was silly of me. The answer is staring me in the face in those logs.
The file path for the temp file begins with file://, whereas my download destination path does not. the answer is to change
NSURL *url = [NSURL URLWithString:self.downloadDestinationPath];
to
NSURL *url = [NSURL fileURLWithPath:self.downloadDestinationPath];
This will give me a valid file path to send the downloaded file to

NSURLSession with UICollectionView

I just read about NSURLSession and I used it to download images from a url.Here i.e. the method that I wrote :
-(void)startDownloadingWithURLString:(NSString*)urlString andDisplayOn:(UIImageView*)imageView{
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession *urlSession = [NSURLSession sessionWithConfiguration:configuration];
NSLog(#"%#",dataResponse);
NSURLSessionDownloadTask *task = [urlSession downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:location]];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = image;
});
}];
[task resume];
}
Now the problem is that I am calling this method inside "cellForItemAtIndexPath:".So overtime I scroll,this method gets called again and again for each cell(reusability concept).Now,as the download process is asynchronous,I can't know when the downloading is complete.
So what should I do to avoid this problem?I just want to download images once.
There are third party libraries available to solve your purpose. I prefer using SDWebImage for the same. It worked like a charm for me.
Just import
#import<SDWebImage/UIImageView+WebCache.h>
and you can use the method to load the web image
sd_setImageWithURL:
It does the same work for you without hustle.
Refer this link https://github.com/rs/SDWebImage
Hope it helps. Hapy Coding..

limit the number of concurrent downloads in AFURLSessionManager

Idea
I'm building files download manager using AFNetworking and I'm using AFURLSessionManager class. the app is suppose to download mp3 files from the server.
I was concerned about memory consuming, so I'm trying to limit the number of simultaneous downloads to 1.
I know that there is a NSOperationQueue property in AFURLSessionManager called operationQueue and it's limited to 1 operation at a time by default.so I'm adding my NSURLSessionDownloadTask to operationQueue.
the problem
the code isn't working. files is being downloaded simultaneously instead of one after another.
the code
// 1. build sessionManager and prepare some vars
// note: by testing i found that it's better to init NSURLSessionConfiguration with backgroundSessionConfigurationWithIdentifier for memory issues
NSURLSessionConfiguration *conf = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"special_Identifier"];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:conf];
NSURL *urlDocs = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory
inDomain:NSUserDomainMask
appropriateForURL:nil
create:NO
error:nil];
__block NSProgress *progress = Nil;
// 2. open sessionManager operation Queue and add this new download
[manager.operationQueue addOperationWithBlock:^{
// 2.1 init new download request
NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:fileLink]];
// 2.2 creat a NSURLSessionDownloadTask
NSURLSessionDownloadTask *downloadTask = [self.downloadManager downloadTaskWithRequest:request progress:&progress
destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
return [urlDocs URLByAppendingPathComponent:fileName];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
if (!error) {
NSLog(#"done: %#", filePath);
}else{
NSLog(#"error %#",error);
}
}];
// 2.3 start downloading
[downloadTask resume];
// 2.4 track downloading progress using KVO
[progress addObserver:self
forKeyPath:NSStringFromSelector(#selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:(__bridge void *)(fileLink)];
}];
In AFNetworking 2 (and AFNetworking 3), you can init your AFHTTPSessionManager with an NSURLSessionConfiguration (use AFHTTPSessionManager initWithBaseURL:sessionConfiguration:). There you can specify the number of connections per host (HTTPMaximumConnectionsPerHost).
Sample:
NSURL *url = [NSURL URLWithString:#"myurl.net"];
NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.defaultSessionConfiguration;
configuration.HTTPMaximumConnectionsPerHost = 1;
AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:url sessionConfiguration:sessionConfiguration];
Documentation:
AFHTTPSessionManager: http://cocoadocs.org/docsets/AFNetworking/3.0.4/Classes/AFHTTPSessionManager.html#//api/name/initWithBaseURL:sessionConfiguration:
NSURLSessionConfiguration: https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionConfiguration_class/#//apple_ref/occ/instp/NSURLSessionConfiguration/HTTPMaximumConnectionsPerHost

Is there something about NSURLSessionConfiguration.HTTPMaximumConnectionsPerHost I am missing?

I am experimenting with replacing some ancient networking code with NSUrlSession, but setting HTTPMaximumConnectionsPerHost to 1 is not having any effect. This request code is called 170 times but it makes 170 connections to the host (watching in CharlesProxy) before anything comes back, which is slamming the server. Am I missing something here?
All requests go to the same domain and url with only differences in parameters. Of course I can do something different but HTTPMaximumConnectionsPerHost seems like it should limit the connections.
At the moment I am compiling versus SDK 7 (due to having to support iOS 6 still) but if I can get this to work I can abandon iOS 6 and just support 7/8 and build vs 8. This is in an enterprise app BTW.
+ (NSURLSession*) sharedSession
{
static NSURLSession* session;
static dispatch_once_t once;
dispatch_once(&once, ^{
NSURLSessionConfiguration * sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 30.0;
sessionConfig.HTTPMaximumConnectionsPerHost = 1;
sessionConfig.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever;
sessionConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:nil
delegateQueue:nil];
});
return session;
}
+ (void) createRequestWithPayload2:(HttpRequestPayload *)payload
success:(void (^)(CommunicationResponse * response))success
failure:(void (^)(NSError * error))failure
progress:(void (^)(TaskStatus status))progressStatus
{
NSURLSession* session = [RequestSender sharedSession];
NSString * url = [NSString stringWithFormat:#"%#/%#", payload.baseURL, payload.urlParams];
NSMutableURLRequest* request = [[NSMutableURLRequest alloc]initWithURL:[NSURL URLWithString:url]];
[request setHTTPMethod:payload.method];
[request setAllHTTPHeaderFields:payload.headers];
if ( payload.body )
{
[request setHTTPBody:payload.body];
}
//NSLog(#"Request:\n%#",request);
NSURLSessionDataTask * task =
[session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *resp, NSError *error)
{
dispatch_async(dispatch_get_main_queue(),
^{
if ( error )
{
NSLog(#"%#",error);
failure(error); }
else
{
NSHTTPURLResponse *response = (NSHTTPURLResponse*) resp;
//NSLog(#"%#",response);
//NSLog(#"%#",data);
CommunicationResponse* cr = [CommunicationResponse new];
[cr set_commStatus:response.statusCode];
[cr set_response:data];
success(cr);
}
});
}];
[task resume];
}
Seems like you're not sharing, but creating new session each time. HTTPMaximumConnectionsPerHost only limit connections on the current session.
From the documentation
This limit is per session, so if you use multiple sessions, your app as a whole may exceed this limit.
NSURLSessionConfiguration : HTTPMaximumConnectionsPerHost
As alternative you can set discretionary property to YES (TRUE). Where it will limit all connections across all sessions to a reasonable number.
NSURLSessionConfiguration : discretionary
Maximum connection per host mean, that more then currently executed connections will be added to queue and wait while older will finish they work.
It limit not all count, but count at one time.

How to cache using NSURLSession and NSURLCache. Not working

I have a test app setup and it successfully downloads content from the network even if the user switches apps while a download is in progress. Great, now I have background downloads in place. Now I want to add caching. There is no point to me downloading images more than once, b/c of system design, given an image URL I can tell you the content behind that URL will never change. So, now I want to cache the results of my download using apple's built in in-memory/on-disk cache that I've read so much about (as opposed to me saving the file manually in NSCachesDirectory and then checking there before making new request, ick). In an attempt to get caching working on top of this working code, I added the following code:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
// Set app-wide shared cache (first number is megabyte value)
[NSURLCache setSharedURLCache:[[NSURLCache alloc] initWithMemoryCapacity:60 * 1024 * 1024
diskCapacity:200 * 1024 * 1024
diskPath:nil]];
return YES;
}
When I create my session, I've added two NEW lines (URLCache and requestCachePolicy).
// Helper method to get a single session object
- (NSURLSession *)backgroundSession
{
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:#"com.example.apple-samplecode.SimpleBackgroundTransfer.BackgroundSession"];
configuration.URLCache = [NSURLCache sharedURLCache]; // NEW LINE ON TOP OF OTHERWISE WORKING CODE
configuration.requestCachePolicy = NSURLRequestReturnCacheDataElseLoad; // NEW LINE ON TOP OF OTHERWISE WORKING CODE
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
});
return session;
}
Then, just to be ultra redundant in an attempt to see caching success I switched my NSURLRequest line from
// NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL]; // Old line, I've replaced this with...
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:2*60]; // New line
Now, when I go to download the item a 2nd time, the experience is exaclty like the first!! Takes a long time to download and progress bar is animated slow and steady like an original download. I want the data in the cache immediately!! What am I missing???
----------------------------UPDATE----------------------------
Okay, thanks to Thorsten's answer, I've added the following two lines of code to my didFinishDownloadingToURL delegate method:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)downloadURL {
// Added these lines...
NSLog(#"DiskCache: %# of %#", #([[NSURLCache sharedURLCache] currentDiskUsage]), #([[NSURLCache sharedURLCache] diskCapacity]));
NSLog(#"MemoryCache: %# of %#", #([[NSURLCache sharedURLCache] currentMemoryUsage]), #([[NSURLCache sharedURLCache] memoryCapacity]));
/*
OUTPUTS:
DiskCache: 4096 of 209715200
MemoryCache: 0 of 62914560
*/
}
This is great. It confirms the cache is growing. I presume since I'm using a downloadTask (downloads to file as opposed to memory), that that's why DiskCache is growing and not memory cache first? I figured everything would go to memory cache until that overflowed and then disk cache would be used and that maybe memory cache was written to disk before the OS kills the app in the background to free up memory. Am I misunderstanding how Apple's cache works?
This is a step forward for sure, but the 2nd time I download the file it takes just as long as the first time (maybe 10 seconds or so) and the following method DOES get executed again:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
// This shouldn't execute the second time around should it? Even if this is supposed to get executed a second time around then shouldn't it be lightning fast? It's not.
// On all subsequent requests, it slowly iterates through the downloading of the content just as slow as the first time. No caching is apparent. What am I missing?
}
What do you make of my edits above? Why am I not seeing the file returned very quickly on subsequent requests?
How can I confirm if the file is being served from the cache on the 2nd request?
Note that the following SO post helped me solve my problem: Is NSURLCache persistent across launches?
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Set app-wide shared cache (first number is megabyte value)
NSUInteger cacheSizeMemory = 500*1024*1024; // 500 MB
NSUInteger cacheSizeDisk = 500*1024*1024; // 500 MB
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:#"nsurlcache"];
[NSURLCache setSharedURLCache:sharedCache];
sleep(1); // Critically important line, sadly, but it's worth it!
}
In addition to the sleep(1) line, also note the size of my cache; 500MB.
According to docs you need a cache size that is way bigger than what you're trying to cache.
The response size is small enough to reasonably fit within the cache.
(For example, if you provide a disk cache, the response must be no
larger than about 5% of the disk cache size.)
So for example if you want to be able to cache a 10MB image, then a cache size of 10MB or even 20MB will not be enough. You need 200MB.
Honey's comment below is evidence that Apple is following this 5% rule. For an 8Mb he had to set his cache size to minimum 154MB.
Solution - first get all info u need it something like this
- (void)loadData
{
if (!self.commonDataSource) {
self.commonDataSource = [[NSArray alloc] init];
}
[self setSharedCacheForImages];
NSURLSession *session = [self prepareSessionForRequest];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[BaseURLString stringByAppendingPathComponent:#"app.json"]]];
[request setHTTPMethod:#"GET"];
__weak typeof(self) weakSelf = self;
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSArray *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
weakSelf.commonDataSource = jsonResponse;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf updateDataSource];
});
}
}];
[dataTask resume];
}
- (void)setSharedCacheForImages
{
NSUInteger cashSize = 250 * 1024 * 1024;
NSUInteger cashDiskSize = 250 * 1024 * 1024;
NSURLCache *imageCache = [[NSURLCache alloc] initWithMemoryCapacity:cashSize diskCapacity:cashDiskSize diskPath:#"someCachePath"];
[NSURLCache setSharedURLCache:imageCache];
}
- (NSURLSession *)prepareSessionForRequest
{
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
[sessionConfiguration setHTTPAdditionalHeaders:#{#"Content-Type": #"application/json", #"Accept": #"application/json"}];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
return session;
}
After you need to download each file - in my case - make parsing of response and download images. Also before making request you need to check if cache already have response for your request - something like this
NSString *imageURL = [NSString stringWithFormat:#"%#%#", BaseURLString ,sourceDictionary[#"thumb_path"]];
NSURLSession *session = [self prepareSessionForRequest];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:imageURL]];
[request setHTTPMethod:#"GET"];
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
if (cachedResponse.data) {
UIImage *downloadedImage = [UIImage imageWithData:cachedResponse.data];
dispatch_async(dispatch_get_main_queue(), ^{
cell.thumbnailImageView.image = downloadedImage;
});
} else {
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
UIImage *downloadedImage = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
cell.thumbnailImageView.image = downloadedImage;
});
}
}];
[dataTask resume];
}
After that you can also check result with xCode Network Analyzer.
Also note as mentionted by #jcaron and documented by Apple
NSURLSession won't attempt to cache a file larger than 5% of the cache
size
Result something like
Once you set the cache and the session, you should use the session-methods to download your data:
- (IBAction)btnClicked:(id)sender {
NSString *imageUrl = #"http://placekitten.com/1000/1000";
NSURLSessionDataTask* loadDataTask = [session dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:imageUrl]] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
UIImage *downloadedImage = [UIImage imageWithData:data];
NSLog(#"ImageSize: %f, %f", downloadedImage.size.width, downloadedImage.size.height);
NSLog(#"DiskCache: %i of %i", [[NSURLCache sharedURLCache] currentDiskUsage], [[NSURLCache sharedURLCache] diskCapacity]);
NSLog(#"MemoryCache: %i of %i", [[NSURLCache sharedURLCache] currentMemoryUsage], [[NSURLCache sharedURLCache] memoryCapacity]);
}];
[loadDataTask resume]; //start request
}
After the first call, the image is cached.

Resources