10

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.

Centurion
  • 13,168
  • 31
  • 99
  • 189

3 Answers3

11

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:

Stephanus Mostert
  • 899
  • 10
  • 11
  • Superb answer and reference links. – Centurion Nov 08 '13 at 08:37
  • Hi guys, I'm having difficulties implementing this solution in my project. I'm also dealing with a server I have no control over, every response has a header "Cache-Control" = "max-age=0, private, no-store, no-cache, must-revalidate"; I've tried implementing this solution, only added some log calls. I can see that cachedResponseForRequest: is being called but storeCachedResponse:forRequest: is never called. What could I be doing wrong? In request I'm setting NSURLRequestReturnCacheDataElseLoad – Nemanja Kovacevic Jun 18 '14 at 09:08
10

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)

    }

}
Vojtech Vrbka
  • 5,175
  • 6
  • 42
  • 62
1

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.

algal
  • 26,206
  • 12
  • 73
  • 78