I'm trying to use NSURLSessionDownloadTask, and take advantage of Apple's in-built URL caching functionality. I have succeeded in getting the caching to work when using an NSURLSessionDataTask using the code below:
- (void)downloadUsingNSURLSessionDataTask:(NSURL *)url {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
[dataTask resume];
}
- (void)cachedDataTaskTest {
// This call performs an HTTP request
[self downloadUsingNSURLSessionDataTask:[NSURL URLWithString:#"http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"]];
[NSThread sleepForTimeInterval:1];
// This call returns the locally cached copy, and no HTTP request occurs
[self downloadUsingNSURLSessionDataTask:[NSURL URLWithString:#"http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"]];
}
However, I need to perform a background download for which I have to use an NSURLDownloadTask. When I switch to this the caching behaviour does not occur.
- (void)downloadUsingNSURLSessionDownloadTask:(NSURL *)url {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
[downloadTask resume];
}
- (void)cachedDownloadTaskTest {
// This call performs an HTTP request
[self downloadUsingNSURLSessionDownloadTask:[NSURL URLWithString:#"http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"]];
[NSThread sleepForTimeInterval:1];
// This call also performs an HTTP request
[self downloadUsingNSURLSessionDownloadTask:[NSURL URLWithString:#"http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"]];
}
This documentation from Apple indicates that NSURLDownloadTasks don't call the URLSession:dataTask:willCacheResponse:completionHandler: delegate method, so it is not possible for your app to hook into the caching life cycle. My guess is that this implies that caching is simply not available for these tasks, but it is not explicit about this.
For a data task, the NSURLSession object calls the delegate’s URLSession:dataTask:willCacheResponse:completionHandler: method. Your
app should then decide whether to allow caching. If you do not
implement this method, the default behavior is to use the caching
policy specified in the session’s configuration object.
Can anyone confirm this hunch that NSURLSessionDownloadTasks simply don't support caching? Is it possible to take advantage of Apple's HTTP caching behaviour in a background task?
NSURLSessionDownloadTask performs work using a system service (daemon) that performs the download outside your application process. Because of this, the delegate callbacks that actually get invoked for a download task are more limited than those for NSURLSessionDataTask. As documented in Life Cycle of a URL Session, a data task delegate will receive callbacks to customize caching behavior, while a download task delegate will not.
A download task should use the caching policy specified by the NSURLRequest, and should use the cache storage specified by the NSURLSessionConfiguration (if it does not, file a bug). The default cache policy is NSURLRequestUseProtocolCachePolicy, and the default URL cache storage is the shared URL cache for non-background and non-ephemeral configurations. The delegate callbacks for URLSession:dataTask:willCacheResponse:completionHandler: are not a good indicator of wether caching is actually occurring.
If you create an NSURLSessionDownloadTask using the default session configuration and do not customize the cache policy of NSURLRequests, caching is already happening.
It looks like NSURLSessionDownloadTask does not cache, by design.
NSURLSessionConfiguration documentation
The defaultSessionConfiguration method is documented:
The default session configuration uses a persistent disk-based cache (except when the result is downloaded to a file) and stores credentials in the user’s keychain.
However, none of the other constructors are documented to exclude the above italicized exception. I also tested backgroundSessionConfigurationWithIdentifier and it doesn't appear to do the job either.
Also, requestCachePolicy doesn't offer any way out of the exception either.
NSURLSessionDownloadTask runtime efficiency
NSURLSessionDownloadTask writes incoming data to a temporary file. When it completes the file, it notifies the delegate or completion handler. Finally it deletes the file.
While it could simply move the file into cache at the end, it would have to deal with either the delegate or completion handler actually modifying the file and thus changing its cached representation, or even moving the file to a permanent location it can't track.
It could copy the file before notifying the delegate or completion handler, but this would be inefficient for large files.
It could keep the file read-only, but doesn't appear to do so on iOS 8.0.
Therefore, it's unlikely that the system would do any caching of download tasks.
Workaround
Your best bet is use NSURLSessionDataTask, then when your delegate's URLSession:dataTask:didReceiveData: method is called, append the incoming data to your own file. The next time you use NSURLSessionDataTask you get the cached data all in one call of URLSession:dataTask:didReceiveData:.
Got it working only by storing the response forcefully
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let uRLCache = URLCache(memoryCapacity: 500 * 1024 * 1024, diskCapacity: 500 * 1024 * 1024, diskPath: nil)
URLCache.shared = uRLCache
}
func download() {
let req = URLRequest(url: remoteurl, cachePolicy: URLRequest.CachePolicy.returnCacheDataElseLoad, timeoutInterval: 30)
if let cachedResponse = URLCache.shared.cachedResponse(for: req) {
print("found")
}
let downloadTask = URLSession.shared.downloadTask(with: req, completionHandler: { [weak self] (url, res, error) -> Void in
guard let `self` = self else { return }
if let data = try? Data(contentsOf: url), res != nil && URLCache.shared.cachedResponse(for: req) == nil {
// store the response
URLCache.shared.storeCachedResponse(CachedURLResponse(response: res!, data: data), for: req)
}
})
downloadTask?.resume()
}
Related
Context:
I try to call a create a task (download or upload) from an action extension, with a backgroundSessionConfiguration.
To do this I fallow the exemple in apple documention
-(void)downloadTest
{
NSURLSession *mySession = [self configureMySession];
NSURL *url = [NSURL URLWithString:#"http://www.sellcell.com/blog/wp-content/uploads/2014/03/dog-apps.jpg"];
NSURLSessionTask *myTask = [mySession downloadTaskWithURL:url];
[myTask resume];
}
- (NSURLSession *) configureMySession {
if (!_mySession) {
NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"com.mycompany.myapp.backgroundsession"];
// To access the shared container you set up, use the sharedContainerIdentifier property on your configuration object.
config.sharedContainerIdentifier = #"group.com.mycompany.appname";
_mySession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return _mySession;
}
My problem is that when I call [mySession downloadTaskWithURL:url]; it returns nil.
If I change the configuration to NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; then a task is created.
I don't see what I'm doing wrong , I have created an app group and I use it the app and in the extension.
I use the group name that I have created in config.sharedContainerIdentifier but I'm not sure it's necessary.
NOTE: I have the same problem with uploadTask.
Are you using ARC? If not, make sure your session is being retained properly. (It looks like you're using an ivar directly.)
Is that shared container identifier correct? If the container isn't in your entitlements or doesn't exist yet, your session will be immediately invalidated. Add an invalidation delegate method and see if it is getting called. If so, that's probably the issue.
iOS 8, XCode 6.3.2
I want to download multiple files serially.
In the wake of the Push notification, APP will start BackgroudDownload by NSURLSessionDownloadTask.
After the First BackgroudDownload process has been completed, APP want to start Second process, but Second BackgroudDownload process does not start.
Code is below
// This method is called by Push Notification
- (void)startBackgroundDownload
{
// Session
NSURLSessionConfiguration *configFirst = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"com.test.first"];
sessionFirst = [NSURLSession sessionWithConfiguration:configFirst delegate:self delegateQueue:nil];
NSURLSessionConfiguration *configSecond = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"com.test.second"];
sessionSecond = [NSURLSession sessionWithConfiguration:configSecond delegate:self delegateQueue:nil];
// Start First Download
NSURLRequest *requestFirst = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://xxxxx/first.zip"]];
NSURLSessionDownloadTask *downloadTaskFirst = [sessionFirst downloadTaskWithRequest:requestFirst];
[downloadTaskFirst resume];
}
// Finish Download
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
if (session == sessionFirst) {
NSURLRequest *requestSecond = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://xxxxx/second.zip"
NSURLSessionDownloadTask *downloadTaskSecond = [sessionSecond downloadTaskWithRequest:requestSecond];
[downloadTaskSecond resume];
} else if (session == sessionSecond) {
NSLog(#"all finish");
}
}
The First is successful, and the Second is fail (not start).
I want advice to pursue the cause.
Thank you for any help you can provide.
downloading task is divide in perfect part like as follow.
First make one array of zip files which you want to download.
Initialise session object
Write one method which can get URL and "startDownloading"
In delegate method (successful download) called unzip that file. remove first object of zip array and again called "startDownloading" method and its call until your array count is greater than zero
I hope you will understand what I want to explain here.
I created a simple NSURLSessionDownloadTask to download from a URL, with its class having the NSURLSession delegates:
#interface DownloadManager : NSObject <NSURLSessionDataDelegate, NSURLSessionDelegate, NSURLSessionDownloadDelegate, NSURLSessionTaskDelegate>
//...
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
[sessionConfiguration setTimeoutIntervalForRequest:30.0];
[sessionConfiguration setTimeoutIntervalForResource:60.0];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:self.url];
[downloadTask resume];
However, I could not find a protocol method that listens to the download task timing out. Is there a way to listen to the timeout (ex. - I wanted to close a progress dialog box when 30.0 seconds have passed and no data is still received)
I've already scavenged Google but haven't found any information so far, so I'll leave this question here while I search for more info.
Thanks so much!
The timeout is one of the errors NSURLSession will give you in completionHandler block. It's NSURLErrorTimedOut = -1001.
in delegate method
- URLSession:task:didCompleteWithError:
check the NSError if it's NSURLErrorTimedOut do what you want
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Constants/#//apple_ref/doc/constant_group/URL_Loading_System_Error_Codes
If the user provides it's own NSURLSessionConfiguration, how do I know if I can ask for a NSURLSessionDownloadTask or NSURLSessionDataTask since a NSURLSessionDataTask can't be created for a background session
You can decide weather the provided NSURLSessionConfiguration object is a background session or not by using its identifier property as
NSURLSessionConfiguration *config = inConfig;
if(config.identifier != nil) {
//Background session configuration
}
else {
// not a Background session configuration
}
- (NSURLSession *)sharedBackgroundSession
{
static NSURLSession *sharedBackgroundSession = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:#"com.example.apple-samplecode.SimpleBackgroundTransfer.BackgroundSession"];
configuration.URLCache = [NSURLCache sharedURLCache];
configuration.requestCachePolicy = NSURLRequestReturnCacheDataElseLoad;
sharedBackgroundSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
});
return sharedBackgroundSession;
}
// When executed on simulator, things work as expected, I never get a nil result.
// When executed on iPhone 5s device, nextDownloadTask is always nil
NSURLSessionDownloadTask *nextDownloadTask = [[self sharedBackgroundSession] downloadTaskWithRequest:request];
Any ideas?
UPDATE 9/12/14:
I had this issue again, googled and found this SO post and then saw that I was the author of the question! This time around -URLSession:task:didCompleteWithError wasn't even getting called. THE SOLUTION WAS TO RESTART MY PHONE. This must be an Apple bug. I'm running iOS7 on my iPhone 5s.
You should implement URLSession:task:didCompleteWithError: and it should report meaningful errors, if any.
In terms of sorts of issues that would work on simulator and not the device, they include (a) use of localhost in the URL; (b) use of URLs only available on LAN and iOS device not connecting successfully via wifi, e.g. perhaps only connecting via cellular; etc. You'd have to share more about the type of URL you are connecting to.
According to the documentation, NSURLErrorDomain error -999 is NSURLErrorCancelled. I'm unclear as to why you would get that particular error in this context, unless you were explicitly canceling the request or invalidating the session.
In my case, I was able to find a workaround for this problem:
1) Call the sharedBackgroundSession function
2) Call downloadTaskWithRequest on that session to test if the session is working properly, i.e. returning something non-nil
3) If #2 fails, then try again after a delay:
[_sharedBackgroundSession performSelector:#selector(sharedBackgroundSession) withObject:nil afterDelay:0.01];
In my case, the first attempt sometimes fails, sometimes succeeds. But the second attempt seems to reliably succeed.
I fixed this by calling [backgroundSession invalidateAndCancel]. I only had to call this once and after that I removed the code.
I'm now wondering if I should detect when tasks can no longer be created and call invalidateAndCancel before trying anymore tasks.
Need to call invalidateAndCancel for your NSURLSession
Set to nil your NSURLSession
Create again NSURLSession
Create again NSURLSessionDownloadTask
e.g.
NSURLSessionDownloadTask *dataTask = [self.yourSession downloadTaskWithRequest:urlRequest];
NSInteger attemptsNumber = 5;
for (NSUInteger attempts = 0; !dataTask && attempts < attemptsNumber; attempts++){
[self.yourSession invalidateAndCancel];
self.yourSession = nil;
NSURLSessionConfiguration *backgroundConfig = [NSURLSessionConfiguration backgroundSessionConfiguration:[[NSBundle mainBundle] bundleIdentifier]];
self.yourSession = [NSURLSession sessionWithConfiguration:backgroundConfig delegate:self delegateQueue:nil];
dataTask = [self.yourSession downloadTaskWithRequest:urlRequest];
}