We have set up a simple NSURLConnection and NSURLCache as per the abbreviated code snippet below. We have made sure, that the server (localhost:9615) returns the following caching headers:
ETag : abcdefgh
Cache-Control : public, max-age=60
However, the willCacheResponse delegate method is never called. Any idea?
Code:
// In the app delegate
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
// Triggered by a UIButton
- (IBAction)sendRequest:(UIButton *)sender {
NSLog(#"sendRequest");
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://localhost:9615"] cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:60.0];
NSMutableData *receivedData = [NSMutableData dataWithCapacity: 0];
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (!theConnection) {
receivedData = nil;
}
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
NSLog(#"willCacheResponse");
// ...
}
Cocoa applies all sorts of criteria to determine whether it can cache. For example, in my experience, you will not see willCacheResponse called if the size of the response exceeds roughly 5% of the persistent storage cache size. I've also seen others claim that if max-age is smaller than 1835400, it won't cache either (this is not my experience, but perhaps older iOS versions suffered from this). Obviously, the request has to be a http or https request, not ftp or file request.
If my cache is large enough (and my response correctly supplies Cache-Control and Content-Type), I find that the cache works properly. I only wish that Apple would clearly articulate the criteria being applied (either in documentation or return codes) as I and others have wasted hours diagnosing cache failures only to realize that Cocoa was applying some secret rules.
Note, if NSURLConnection was, itself, satisfied by retrieving it from cache, willCacheResponse will not be called. And, of course ensure that didFailWithError was not called.
Related
I have seen several posts for this question and I cant figure out how to get this working. For example, this post NSCachedURLResponse returns object, but UIWebView does not interprets content
suggests that we subclass and override cachedResponseForRequest and modify the NSURLRequest before returning a cached response (not sure why this needs to be done).
This is the code i have in my NSURLCache subclass:
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
NSCachedURLResponse *resp = [super cachedResponseForRequest:request];
if (resp != nil && resp.data.length > 0) {
NSDictionary *headers = #{#"Access-Control-Allow-Origin" : #"*", #"Access-Control-Allow-Headers" : #"Content-Type"};
NSHTTPURLResponse *urlresponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:#"1.1" headerFields:headers];
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:urlresponse data:resp.data];
return cachedResponse;
} else {
return nil;
}
}
Now when i load the request via the webview and I'm offline, the delegate didFailLoadWithError gets called (it was not able to load), but before that delegate method is called, my cachedResponseForRequest method gets called and the cached response has some html however my didFailLoadWithError method still gets called. So my question is, should the webViewDidFinishLoad should get called if we return the cached response from the NSURLCache, if not how do i make this work?
I have this in my didFinishLaunchingWithOptions:
NSUInteger cacheSizeMemory = 500*1024*1024; // 500 MB
NSUInteger cacheSizeDisk = 500*1024*1024; // 500 MB
NSURLCache *sharedCache = [[CustomCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:#"nsurlcache"];
[NSURLCache setSharedURLCache:sharedCache];
This is the flow i want working:
Launch the app.
Hit some url (lets say slickdeals.net)
Go offline kill the application
Launch the application
UIWebView attempts to hit url but device is offline and a returned cached response is returned.
We display cached response.
Any thoughts on what needs to change in my code is appreciated!
NSURLCache is designed for HTTP conform caching, and often behaves unexpectly. You must wrote your own cache for your use case, and implement a custom NSURLProtocol to use it. The protocol put the responses into your cache, and provides them from cache when offline. Rob Napier wrotes an excellent article about this.
I initialized NSURLCache in my app delegate like so:
int cacheSizeMemory = 4*1024*1024; // 4MB
int cacheSizeDisk = 32*1024*1024; // 32MB
MyServerCache *sharedCache = [[MyServerCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:#"nsurlcache"];
[MyServerCache setSharedURLCache:sharedCache];
Then I create a request with default cache policy and call:
[NSURLConnection sendSynchronousRequest: urlRequest
returningResponse: &response
error: pError ];
The response is of type "application/json" and has "Cache-Control" set to "private, max-age=600". The response is only 874 bytes (shouldn't be too large to cache), however NSURLCache does not seem to work.
I have run this on a simulator and looked for the on-disk cache which doesn't seem to exist, so I subclassed NSURLCache (MyServerCache) and overrode the following methods to see if they were being called:
-(NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
NSLog(#"Returning cached response for request: %#", request);
return [super cachedResponseForRequest:request];
}
-(void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request
{
NSLog(#"Caching response for request: %#", request);
[super storeCachedResponse:cachedResponse forRequest:request];
}
No matter what I do (and I've tried several different API's and responses), NSURLCache never seems to cache anything. I can never find the on-disk cache in the file system, and the methods above are never called on my subclass of NSURLCache (MyServerCache).
I'd really like to use this rather than rolling my own cache, but I have no idea why it's not working or how to get it to work. Please help.
You have to create your request with the NSURLRequestUseProtocolCachePolicy and it works perfectly out-of-the box with NSURLCache.
NSURLRequestCachePolicy cachePolicy = NSURLRequestUseProtocolCachePolicy;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:cachePolicy
You can read about the NSURLRequestCachePolicy here: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSURLRequest_Class/#//apple_ref/c/tdef/NSURLRequestCachePolicy
A little bit confusing ist that iOS always returns a 200 - Success so you will never get a 304 - Resource not Modified when a response is coming from cache.
So it´s more easy to see the cache policy working when monitoring the server side.
You can use https://github.com/PaulEhrhardt/Simple-Cache-Test to validate cache-control, expires and ETag with a little local ruby server. It´s just a few lines to set-up. I tested both successfully with 7.1 and 8.4 iOS SDK.
I understand the need of image compression while downloading images when connected to a 3G network, but I am getting really bad looking images... I'm caching downloaded images and I realized that the quality of the images depends on the active connection. My code:
KTMember *member = [[DataManager sharedManager] getMemberWithId:memberId];
if (member) {
NSLog(#"caching member %d locally",member.memberId);
memberImg = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:member.imageUrl]]];
[[DataManager sharedManager] saveImageToDocuments:memberImg withId:memberId];
return memberImg;
} else {
return nil;
}
So the question is - is there any way of overriding the image compression even though the active network is 3G?
Thanks
There is no global mechanism that adaptively increases image compression for slow connections. What you describe would require custom code on the server, and would vary from server to server.
What service is providing these images?
EDIT: Thank you for fixing my answer, There are some mechanism of image compressing by Verizon network optimization.
I think,The quality of image, in the term of byte stream, depend on the server provide whether or not compression.
But there is some solution. you can also implement NSURLConnectionDataDelegate to handle the data from URL request with Thread Programming. There is interesting method :
/** connection:didReceiveResponse: is called when
* enough data has been read to construct an
* NSURLResponse object. In the event of a protocol
* which may return multiple responses (such as HTTP
* multipart/x-mixed-replace) the delegate should be
* prepared to inspect the new response and make
* itself ready for data callbacks as appropriate.
**/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
/** connection:didReceiveData: is called with a single
* immutable NSData object to the delegate,
* representing the next portion of the data loaded
* from the connection. This is the only guaranteed
* for the delegate to receive the data from the
* resource load
**/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
/** connection:willCacheResponse: gives the delegate
* an opportunity to inspect and modify the
* NSCachedURLResponse which will be cached by the
* loader if caching is enabled for the original
* NSURLRequest. Returning nil from this delegate
* will prevent the resource from being cached. Note
* that the -data method of the cached response may
* return an autoreleased in-memory copy of the true
* data, and should not be used as an alternative to
* receiving and accumulating the data through
* connection:didReceiveData
**/
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
/** connectionDidFinishLoading: is called when all
* connection processing has completed successfully,
* before the delegate is released by the
* connection
**/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
You can also manage your data in didReceiveData with accumulate each incoming data and when complete downloading , In connectionDidFinishLoading , you could deal with the NSData of image that you receive all.
Hope it helps you.
I am using NSURLRequest with CachePolicy to download a plist in NSData. When I change the content of my plist my app is ignoring this and still presents the content which is cached. How long does the cache persist? If so is there an option to say how long the cache data persists?
Is there a way to check in NSURLRequest if the data on the server is newer than the cache load the data from the server or if it is equal to cache use the cache?
Have a look at Controlling Response Caching in the URLLoadingSystem docs.
You can add your own date in the delegate methods
-(NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse
Much more easy with the caching system is ASIHTTPRequest. I recommend to use this URL Loading System.
From the apple docs:
The example in Listing 6 prevents the caching of https responses. It
also adds the current date to the user info dictionary for responses
that are cached.
-(NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
NSCachedURLResponse *newCachedResponse = cachedResponse;
if ([[[[cachedResponse response] URL] scheme] isEqual:#"https"]) {
newCachedResponse = nil;
} else {
NSDictionary *newUserInfo;
newUserInfo = [NSDictionary dictionaryWithObject:[NSCalendarDate date]
forKey:#"Cached Date"];
newCachedResponse = [[[NSCachedURLResponse alloc]
initWithResponse:[cachedResponse response]
data:[cachedResponse data]
userInfo:newUserInfo
storagePolicy:[cachedResponse storagePolicy]]
autorelease];
}
return newCachedResponse;
}
I'm currently trying to log all URL connections made in my app. Is there a debug switch, or network activity monitor tool I can use to do this?
Or is my only alternative to include NSLog statements throughout the source base?
Thanks,
Ash
All NSURLConnections of your application use a shared cache class for default
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSURLCache_Class/Reference/Reference.html
So, one thing you could do is subclass the default cache, and then at the cachedResponseForRequest NSURLCache method, you can to track you requests.
#interface CustomNSURLCache : NSURLCache {
}
#end
#implementation CustomNSURLCache
-(NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
NSLog(#"connection will send request for url: %#", request);
return [super cachedResponseForRequest:request];
}
#end
At your AppDelegate didFinishLaunchingWithOptionsmethod, set the shared cache to an instance of your cache.
CustomNSURLCache *customCache = [[CustomNSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:51200 diskPath:nil];
[NSURLCache setSharedURLCache:customCache];
[customCache release];
(being 0 the default value for MemoryCapacity and 512000 for DiskCapacity)
Now when you create a new connection
NSURLRequest *request1 = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:#"http://stackoverflow.com"]];
[[NSURLConnection alloc] initWithRequest:request1 delegate:self];
you should see something like this at your console
connection will send request for url: <NSURLRequest https://stackoverflow.com/>
In Instruments there is a Network Activity Monitor under the System Instruments. I have not personally used it though so I don't know if it does what you want.