I've created an NSURLCache subclass which forces caching of responses for a designated amount of time. This is working well, and cached items are expired as expected. However I'm running into issues when trying to forcefully remove a cached response using NSURLCache's removeCachedResponseForRequest: method.
What I'm looking to achieve is to allow users to force an immediate reload of remote data. To do this, I pass an "ignoreCache" flag when making a request. I construct my NSURLRequest as normal, but ask my NSURLCache to remove any previously cached responses for the given request. This doesn't seem to have any effect, however, as the cached result is still present and used when the request is executed.
The documentation around NSURLCache is fairly sparse. The NSURLCache headers state that the NSURLRequest passed to removeCachedResponseForRequest: is used as a key to lookup the associated NSCachedURLResponse object, but there's little information given as to the logistics of that comparison. Does the NSURLCache class expect to receive the same NSURLRequest instance that generated the cached response, or does it simply compare the NSURL it represents?
Hopefully someone can point me in the right direction.
var error: NSError? = nil
var URLRequest: NSMutableURLRequest = self.operationManager.requestSerializer.requestWithMethod("GET", URLString: NSURL(string: request.URL, relativeToURL: self.operationManager.baseURL).absoluteString, parameters: request.parameters, error: &error)
URLRequest.cachePolicy = NSURLRequestCachePolicy.ReturnCacheDataElseLoad
// We've been asked to ignore the cache, so remove our previously cached response
if ignoreCache == true {
NSURLCache.sharedURLCache().removeCachedResponseForRequest(URLRequest)
}
Here's the Swift code from my NSURLCache subclass for reference:
// MARK: - NSURLCache
override func cachedResponseForRequest(request: NSURLRequest!) -> NSCachedURLResponse! {
var cachedResponse: NSCachedURLResponse? = super.cachedResponseForRequest(request)
if(cachedResponse != nil && cachedResponse!.userInfo != nil) {
var cacheDate: NSDate? = cachedResponse!.userInfo![self.cacheExpirationKey] as? NSDate
if(cacheDate != nil) {
var cacheDateLimit: NSDate = cacheDate!.dateByAddingTimeInterval(self.cacheExpirationInterval)
if(cacheDate!.compare(NSDate()) == NSComparisonResult.OrderedAscending) {
self.removeCachedResponseForRequest(request)
} else {
return cachedResponse
}
}
}
// Either our cached data was too old, or we don't have any that match the NSURLRequest
return nil
}
override func storeCachedResponse(cachedResponse: NSCachedURLResponse!, forRequest request: NSURLRequest!) {
var userInfo: NSMutableDictionary = NSMutableDictionary(dictionary: cachedResponse.userInfo)
userInfo[cacheExpirationKey] = NSDate().dateByAddingTimeInterval(self.cacheExpirationInterval)
var modifiedCacheResponse: NSCachedURLResponse = NSCachedURLResponse(response: cachedResponse.response, data: cachedResponse.data, userInfo: userInfo, storagePolicy: cachedResponse.storagePolicy)
super.storeCachedResponse(modifiedCacheResponse, forRequest: request)
}
Assuming you are on iOS 8, removeCachedResponseForRequest: does not work.
See http://blog.airsource.co.uk/2014/10/13/nsurlcache-ios8-broken-2/
Related
I've been banging my head against the while now trying to figure out what's going on with NSURLCache.
Basically, the server I am connecting to doesn't set any cache control headers... So following various guides and apple docs (ie https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/URLLoadingSystem/Concepts/CachePolicies.html) I set my own cache control headers in the willCacheResponse delegate and then return that modified response in the completion handler. The apple docs and the resource I've read seem to indicate this should work. But what I'm seeing is that the cached data is returned after it should be expired based on max-age. It seems like max-age is being ignored and the NSURLCache is using another heuristic to determine if it should pull data from the cache or not.
I set the max-age=60 cache-control header and I've verified using Charles that the data is pulled from cache and no network requests are made long after 60 seconds. Eventually (seems non-deterministic) a new request will be made that actually goes to the server (usually after a few hours have passed and I try the request again).
Here is the code, for testing purposes I'm just hardcoding the max-age to 60 seconds:
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: #escaping (CachedURLResponse?) -> Void) {
var modifiedReponse: URLResponse? = nil
if let HTTPResponse = proposedResponse.response as? HTTPURLResponse {
if var newHeaders = HTTPResponse.allHeaderFields as? [String : String] {
if newHeaders["Cache-Control"] == nil {
newHeaders["Cache-Control"] = "max-age=60"
}
modifiedReponse = HTTPURLResponse(url: HTTPResponse.url!, statusCode: HTTPResponse.statusCode, httpVersion: "HTTP/1.1", headerFields: newHeaders)
}
}
let response = modifiedReponse ?? proposedResponse.response
var newCachedResponse: CachedURLResponse? = nil
newCachedResponse = CachedURLResponse(response: response, data: proposedResponse.data, storagePolicy: proposedResponse.storagePolicy)
}
There are a few optional checks in there but I have confirmed that the response on newCachedResponse I am returning has the cache control header set to max-age=60. Am I doing something obviously wrong here? Or is NSURLCache just F'd?
I know this is super late, but I think you just need to pass that newCachedResponse back into the completion handler closure provided to you.
Such as the last line being:
completionHandler(newCachedResponse)
I'm using the following code to test a behavior in NSURLCache. I initialize an API instance in AppDelegate. I configure the manager according to Alamofire's documentation, I configure the shared cache, and I assign dataTaskWillCacheResponse to make sure that the response will indeed be cached.
Then I call makeRequest which checks if a cached response exists (which it shouldn't on the first launch) and then I use my manager to make a request using the same URL so that the request is equivalent throughout the test.
My breakpoint at dataTaskWillCacheResponse is hit, I continue, the responseJSON block is executed and is Successful so I performTests using the request.
First, I check if the response is cached. It is: good!
Second, (and this is the problem) I remove the cached response for that request and then check if it exists. It does: bad!
Third, I check if removing all cached responses will remove that response. It does: good! But it's odd that that worked and the previous attempt at just removing the single response didn't...
Here's the code:
import Alamofire
class API: Manager.SessionDelegate {
var manager: Manager!
override init() {
super.init()
manager = Manager(session: urlSession(), delegate: self)
configureCache(memoryCapacityMB: 5, diskCapacityMB: 25)
manager.delegate.dataTaskWillCacheResponse = { urlSession, dataTask, cachedResponse in
// Placing a breakpoint here confirms that the response is going to be cached
return cachedResponse
}
}
private func urlSession() -> NSURLSession {
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
return NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
private func configureCache(memoryCapacityMB memory: Int, diskCapacityMB disk: Int) {
let memoryCapacity = memory * 1024 * 1024
let diskCapacity = disk * 1024 * 1024
let sharedCache = NSURLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: nil)
NSURLCache.setSharedURLCache(sharedCache)
}
// MARK: Request
func makeRequest() {
// The response should be nil on the first launch since nothing has been cached
let request = NSURLRequest(URL: NSURL(string: "http://jsonplaceholder.typicode.com/posts")!)
let response = NSURLCache.sharedURLCache().cachedResponseForRequest(request)
print(response)
manager.request(.GET, request.URLString).responseJSON { response in
switch response.result {
case .Success:
self.performTests(with: response.request!)
case .Failure:
break
}
}
}
func performTests(with request: NSURLRequest) {
// Should exist
var response = NSURLCache.sharedURLCache().cachedResponseForRequest(request)
print(response)
// And it does: good!
// Remove the cached resopnse and check if it exists
NSURLCache.sharedURLCache().removeCachedResponseForRequest(request)
response = NSURLCache.sharedURLCache().cachedResponseForRequest(request)
print(response)
// And it does: bad!
// Try removing all cached responses and check if it exists
NSURLCache.sharedURLCache().removeAllCachedResponses()
response = NSURLCache.sharedURLCache().cachedResponseForRequest(request)
print(response)
// And it doesn't: good! But odd...
}
}
So how does one remove the cached response of a single request then? And is this unintended behavior? Or is NSURLCache behaving correctly and I'm just missing something? Thanks ahead of time for taking a look!
My recollection is that most URL cache changes are not synchronous. They only actually happen after you return to the run loop and allow various asynchronous callbacks to occur.
Try running the rest of the code asynchronously after a delay of 3-5 seconds and see if the request has been removed.
If that doesn't fix the problem, file a bug.
Is it possible to use NSURLCache to cache responses when the URL includes a changing query item? For example, we add Mashery's required "sig=XXXXXX" query item, which changes for each request.
If not, is there a workaround?
Solved by subclassing NSURLCache and overriding its caching methods.
In each overridden method, I remove the query item from the request prior to calling the superclass' method.
For example:
override func storeCachedResponse(cachedResponse: NSCachedURLResponse, forRequest request: NSURLRequest) {
let strippedRequest = removeQueryItemFromRequest(self.queryItemName, request: request)
if let url = strippedRequest.URL {
let response = NSURLResponse(URL: url, MIMEType: cachedResponse.response.MIMEType, expectedContentLength: Int(cachedResponse.response.expectedContentLength), textEncodingName: cachedResponse.response.textEncodingName)
let newCachedResponse = NSCachedURLResponse(response: response, data: cachedResponse.data)
super.storeCachedResponse(newCachedResponse, forRequest: strippedRequest)
}
else {
super.storeCachedResponse(cachedResponse, forRequest: request)
}
}
self.queryItemName is a stored property passed in to a custom initializer.
I make few NSURLSession requests in a loop. I'd like to store results from responses in the same order as tasks are created. But since completion handler runs in a separate thread it sometimes happens that the response to the second task gets received before the response to the first task.
How to make sure that I get responses in same order as tasks are being started?
var recivedData = [String]()
for index in 0 ... urlsAsString.count-1 {
let myUrl = NSURL(string: urlsAsString[index])
var request = NSMutableURLRequest(URL: myUrl!)
// here I also set additional parameters (HTTPMethod, ...)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
responseData, response, error in
// here I handle the response
let result = ...
dispatch_async(dispatch_get_main_queue()) {
self.recivedData.append("\(result)") // save the result to array
}
}
task.resume()
}
While I'd discourage behaviour that requires responses to be received in a specific order, you can collate the responses (regardless of the order they are received) into a known order.
The receivedData array should be initialised with a capacity that matches the number of requests that will be made:
var receivedData = [String](count: urlsAsString.count, repeatedValue: "")
Then when you receive the response, since you're in a block that has access to the index of the request you can add the response data directly into the index of the receivedData array:
receivedData[index] = result as (String)
The full code is as follows:
var receivedData = [String](count: urlsAsString.count, repeatedValue: "")
for index in 0 ... urlsAsString.count-1 {
let myUrl = NSURL(string: urlsAsString[index])
var request = NSMutableURLRequest(URL: myUrl!)
// here I also set additional parameters (HTTPMethod, ...)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
responseData, response, error in
// here I handle the response
let result = ...
dispatch_async(dispatch_get_main_queue()) {
// Insert the result into the correct index
receivedData[index] = result
}
}
task.resume()
}
Because you know the exact number of http requests.
You can create an array to the size of urls.count, and then set the result in completion handler, corresponding to the index in each loop.
receivedData = [String](count: urls.count, repeatedValue: "No Data")
for (index,url) in enumerate(urls){
let url = NSURL(string: url)!
let task = NSURLSession.sharedSession().dataTaskWithURL(url){ data, response, error in
if error != nil{
self.receivedData[index] = "error: \(error.localizedDescription)"
return
}
let result = ...
dispatch_async(dispatch_get_main_queue()) {
self.recivedData[index] = "\(result)"
}
}
task.resume()
}
Actually, can not make sure the order of responses.
There are two workarounds for this:
Send the requests one after another, which means send the next request after the previous response is returned. You can use ReactiveCocoa to make your codes elegant. Or use the networking library I wrote STNetTaskQueue, there is a STNetTaskChain which can handle the requests one after another.
Send the requests parallel(is what you are doing now), and use a NSDictionary to keep track on the request and response, after all requests is finished, combine the response in the original order.
Hope this helps!
I need to check if a website is reachable before loading it. I am new in iOS developement but this is the method I've implemented to discover the response.
var url = NSURL(string: "http://www.apple.com")
var task = NSURLSession.sharedSession().dataTaskWithURL(url!) {
data, response, error in
println(data)
var httpResponse = response as? NSHTTPURLResponse
println(httpResponse)
}
task.resume()
It works! But the problem is that the response comes from the cache... So the result is that:
If I am checking if a file exists and at that moment I am checking it exists -> for the application it will always exist because it is stored in the cache... So if I remove the file and then I make the request... it will always give me response 200 and not 404.
Infact if I insert this line of code (it deletes the cache!) before making the request then it works like it should work and it always check for real if the website or the file exists!
NSURLCache.sharedURLCache().removeAllCachedResponses()
So... how can I solve this problem in Swift?...thank you very much
You can set a no cache policy by using a new url session instance.
Create a property and set a new NSURLSession instance to it.
var urlSession : NSURLSession!
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.requestCachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalCacheData;
self.urlSession = NSURLSession(configuration: configuration)
Use this URLSession property to get your data.
var url = NSURL(string: "http://www.apple.com")
var task = self.urlSession.dataTaskWithURL(url!) {
data, response, error in
// Your code
}
task.resume()