All http responses from a server come with the headers that inform our app not to cache the responses:
Cache-Control: no-cache
Pragma: no-cache
Expires: 0
So, if you are making NSUrlRequests with default cache policy "NSURLRequestUseProtocolCachePolicy" then the app will always load data from the server. However, we need to cache the responses and the obvious solution would be to set these headers to some time for example (at backend side), set to 10 seconds. But I'm interested in a solution how to bypass this policy and cache every request for 10 seconds.
For that you need to setup shared cache. That might be done in AppDelegate didFinishLaunchingWithOptions:
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024
diskCapacity:20 * 1024 * 1024
diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
Then, we need to embed our code to force to cache a response. If you use an instance of AFHttpClient then it can be done by overriding the method below and manually storing the cache into the shared cache:
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse {
NSMutableDictionary *mutableUserInfo = [[cachedResponse userInfo] mutableCopy];
NSMutableData *mutableData = [[cachedResponse data] mutableCopy];
NSURLCacheStoragePolicy storagePolicy = NSURLCacheStorageAllowedInMemoryOnly;
// ...
return [[NSCachedURLResponse alloc] initWithResponse:[cachedResponse response]
data:mutableData
userInfo:mutableUserInfo
storagePolicy:storagePolicy];
}
And the last thing is to set cachePolicy for the requests. In our case we want to set the same cache policy to all requests. So again, if you use an instance of AFHttpClient then it can be done by overriding the method below:
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters {
NSMutableURLRequest *request = [super requestWithMethod:method path:path parameters:parameters];
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
return request;
}
So far so good. "NSURLRequestReturnCacheDataElseLoad" makes to perform request first time and load response from cache all other times. The problem is, it's unclear how to set cache expiration time, for example 10 seconds.
You can implement a custom NSURLCache that only returns cached responses that has not expired.
Example:
#import "CustomURLCache.h"
NSString * const EXPIRES_KEY = @"cache date";
int const CACHE_EXPIRES = -10;
@implementation CustomURLCache
// static method for activating this custom cache
+(void)activate {
CustomURLCache *urlCache = [[CustomURLCache alloc] initWithMemoryCapacity:(2*1024*1024) diskCapacity:(2*1024*1024) diskPath:nil] ;
[NSURLCache setSharedURLCache:urlCache];
}
-(NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
NSCachedURLResponse * cachedResponse = [super cachedResponseForRequest:request];
if (cachedResponse) {
NSDate* cacheDate = [[cachedResponse userInfo] objectForKey:EXPIRES_KEY];
if ([cacheDate timeIntervalSinceNow] < CACHE_EXPIRES) {
[self removeCachedResponseForRequest:request];
cachedResponse = nil;
}
}
return cachedResponse;
}
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request {
NSMutableDictionary *userInfo = cachedResponse.userInfo ? [cachedResponse.userInfo mutableCopy] : [NSMutableDictionary dictionary];
[userInfo setObject:[NSDate date] forKey:EXPIRES_KEY];
NSCachedURLResponse *newCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:cachedResponse.response data:cachedResponse.data userInfo:userInfo storagePolicy:cachedResponse.storagePolicy];
[super storeCachedResponse:newCachedResponse forRequest:request];
}
@end
If this does not give you enough control then I would implement a custom NSURLProtocol with a startLoading method as below and use it in conjunction with the custom cache.
- (void)startLoading
{
NSMutableURLRequest *newRequest = [self.request mutableCopy];
[NSURLProtocol setProperty:@YES forKey:@"CacheSet" inRequest:newRequest];
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request];
if (cachedResponse) {
[self connection:nil didReceiveResponse:[cachedResponse response]];
[self connection:nil didReceiveData:[cachedResponse data]];
[self connectionDidFinishLoading:nil];
} else {
_connection = [NSURLConnection connectionWithRequest:newRequest delegate:self];
}
}
Some links:
- Useful info on NSURLCache
- Creating a custom NSURLProtocol
If somebody would be interested, here is Stephanus answer rewritten in Swift:
class CustomURLCache: NSURLCache {
// UserInfo expires key
let kUrlCacheExpiresKey = "CacheData";
// How long is cache data valid in seconds
let kCacheExpireInterval:NSTimeInterval = 60*60*24*5;
// get cache response for a request
override func cachedResponseForRequest(request:NSURLRequest) -> NSCachedURLResponse? {
// create empty response
var response:NSCachedURLResponse? = nil
// try to get cache response
if let cachedResponse = super.cachedResponseForRequest(request) {
// try to get userInfo
if let userInfo = cachedResponse.userInfo {
// get cache date
if let cacheDate = userInfo[kUrlCacheExpiresKey] as NSDate? {
// check if the cache data are expired
if (cacheDate.timeIntervalSinceNow < -kCacheExpireInterval) {
// remove old cache request
self.removeCachedResponseForRequest(request);
} else {
// the cache request is still valid
response = cachedResponse
}
}
}
}
return response;
}
// store cached response
override func storeCachedResponse(cachedResponse: NSCachedURLResponse, forRequest: NSURLRequest) {
// create userInfo dictionary
var userInfo = NSMutableDictionary()
if let cachedUserInfo = cachedResponse.userInfo {
userInfo = NSMutableDictionary(dictionary:cachedUserInfo)
}
// add current date to the UserInfo
userInfo[kUrlCacheExpiresKey] = NSDate()
// create new cached response
let newCachedResponse = NSCachedURLResponse(response:cachedResponse.response, data:cachedResponse.data, userInfo:userInfo,storagePolicy:cachedResponse.storagePolicy)
super.storeCachedResponse(newCachedResponse, forRequest:forRequest)
}
}
Another possible solution is to modify the response object and clobber the Cache-Control headers from the server and replace them with your own desired values.
There are two places you could do that.
You could do it in a NSURLSessionDataDelegate
in func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, willCacheResponse proposedResponse: NSCachedURLResponse, completionHandler: (NSCachedURLResponse?) -> Void)
, but if you do it there, then can no longer use the usual completion handler-based methods getting results from session tasks.
Another place you could do it is by defining a custom NSURLProtocol
, which intercepts HTTP and HTTPS responses and modifies them.