14

I'm using AFNetworking and need to cache data in one response for a several minutes. So I set NSUrlCache in app delegate and then in my request setting up it:

NSMutableURLRequest *request = //obtain request; 
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;

How then set expiration date: if the data was loaded more than n minutes ago, ask response from server and not from disk?

EDIT:

Assume that server doesn't support caching, I need to manage it in code.

Honey
  • 24,125
  • 14
  • 123
  • 212
HotJard
  • 3,910
  • 2
  • 31
  • 31

3 Answers3

19

So, I found the solution.

The idea is to use connection:willCacheResponse: method. Before cache the response it will be executed and there we can change response and return new, or return nil and the response will not be cached. As I use AFNetworking, there is a nice method in operation:

- (void)setCacheResponseBlock:(NSCachedURLResponse * (^)(NSURLConnection *connection, NSCachedURLResponse *cachedResponse))block;

Add code:

  [operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
    if([connection currentRequest].cachePolicy == NSURLRequestUseProtocolCachePolicy) {
      cachedResponse = [cachedResponse responseWithExpirationDuration:60];
    }
    return cachedResponse;
  }];

Where responseWithExpirationDuration from category:

@interface NSCachedURLResponse (Expiration)
-(NSCachedURLResponse*)responseWithExpirationDuration:(int)duration;
@end

@implementation NSCachedURLResponse (Expiration)

-(NSCachedURLResponse*)responseWithExpirationDuration:(int)duration {
  NSCachedURLResponse* cachedResponse = self;
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response];
  NSDictionary *headers = [httpResponse allHeaderFields];
  NSMutableDictionary* newHeaders = [headers mutableCopy];

  newHeaders[@"Cache-Control"] = [NSString stringWithFormat:@"max-age=%i", duration];
  [newHeaders removeObjectForKey:@"Expires"];
  [newHeaders removeObjectForKey:@"s-maxage"];

  NSHTTPURLResponse* newResponse = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL
                                                               statusCode:httpResponse.statusCode
                                                              HTTPVersion:@"HTTP/1.1"
                                                             headerFields:newHeaders];

  cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:newResponse
                                                            data:[cachedResponse.data mutableCopy]
                                                        userInfo:newHeaders
                                                   storagePolicy:cachedResponse.storagePolicy];
  return cachedResponse;
}

@end

So, we set expiration in seconds in http header according to http/1.1 For that we need one of headers to be set up: Expires, Cache-Control: s-maxage or max-age Then create new cache response, because the properties is read only, and return new object.

HotJard
  • 3,910
  • 2
  • 31
  • 31
  • This will not work in subsequent request when server returns 304 (not modified). Note that setCacheResponseBlock is called in AFNetworking only on 200 return codes and not for 304. So for subsequent requests, if the server determines the resource has not changed (304), you will still end up making subsequent requests until server returns 200 the next time. – Nikhil Lele Apr 24 '15 at 21:21
  • How can I call `setCacheResponseBlock` if I use `UIImageView (_AFNetworking)` to `setImageWithURLRequest`, because it don't public `af_imageRequestOperation`, and don't have function to set cache block? – huync Aug 19 '15 at 19:16
  • 2
    @huync I don't use AFNetworking for image loading, instead SDWebImage library it has nice build-in image caching system – HotJard Aug 20 '15 at 18:07
8

Swift equivalent of @HotJard's solution using URLSession

extension CachedURLResponse {
    func response(withExpirationDuration duration: Int) -> CachedURLResponse {
        var cachedResponse = self
        if let httpResponse = cachedResponse.response as? HTTPURLResponse, var headers = httpResponse.allHeaderFields as? [String : String], let url = httpResponse.url{

            headers["Cache-Control"] = "max-age=\(duration)"
            headers.removeValue(forKey: "Expires")
            headers.removeValue(forKey: "s-maxage")

            if let newResponse = HTTPURLResponse(url: url, statusCode: httpResponse.statusCode, httpVersion: "HTTP/1.1", headerFields: headers) {
            cachedResponse = CachedURLResponse(response: newResponse, data: cachedResponse.data, userInfo: headers, storagePolicy: cachedResponse.storagePolicy)
            }
        }
        return cachedResponse
    }
}

Then implement URLSessionDataDelegate protocol in your custom class

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) {

    if dataTask.currentRequest?.cachePolicy == .useProtocolCachePolicy {
        let newResponse = proposedResponse.response(withExpirationDuration: 60)
        completionHandler(newResponse)
    }else {
        completionHandler(proposedResponse)
    }
}

Don't forget to create your configuration and session, passing in the your custom class as the delegate reference e.g.

let session = URLSession(
        configuration: URLSession.shared.configuration,
        delegate: *delegateReference*,
        delegateQueue: URLSession.shared.delegateQueue
    )
let task = session.dataTask(with: request)
task.resume()
Anwuna
  • 887
  • 9
  • 17
  • Great!. Is it possible to use this with AlamoFire? – rmvz3 Mar 11 '18 at 21:33
  • I haven't really used AlamoFire for a production app so I'm not quite sure. But I'd assume the same principle should be applicable. – Anwuna Mar 12 '18 at 11:35
  • 1
    From AlamoFire's documentation, you can override the closure - open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)? on the SessionManager's SessionDelegate of AlamoFire which maps to URLSession's Overrides urlSession(_:dataTask:willCacheResponse:completionHandler:) and implement the same logic as above there. See https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#session-manager – Anwuna Mar 12 '18 at 11:46
  • I've done that already. Cache-Control header is correctly added to cached response in alamofire delegate but service is always called besides using .useProtocolCachePolicy in the request. I'll keep researching on this – rmvz3 Mar 12 '18 at 17:35
  • I like this solution, although I think I only partly understand it. Do you rely on 'willCacheResponse' to be called every time a new response arrives? does this always happen? what about that "304" server reply (unmodified) someone referred to? Then - Can I somehow, in that context, decide NOT to cache certain responses, based on NSURLRequest parameters? In our case, server does not cache responses, but provides somewhere in the response body what is the "lifespan" of the response, and sometimes - it is for "one time use". – Motti Shneor Sep 17 '18 at 12:31
  • @MottiShneor yes the method *urlSession(_:dataTask:willCacheResponse:completionHandler:)* is called every time a new response arrives. To answer your question on how to decide not to cache certain responses, you can modify the method *urlSession(_:dataTask:willCacheResponse:completionHandler:)*. The UrlRequest can be retrieved from dataTask.currentRequest property. – Anwuna Sep 18 '18 at 18:58
1

The expiration of responses in the NSURLCache is controlled via the Cache-Control header in the HTTP response.

EDIT I see you've updated your question. If the server doesn't provide the Cache-Control header in the response, it won't be cached. Every request to that endpoint will load the endpoint rather than return a cached response.

neilco
  • 7,699
  • 2
  • 33
  • 41
  • 1
    The question is how to set expiration when server doesn't support caching – HotJard Nov 08 '13 at 10:14
  • If the server doesn't provide the Cache-Control header in the response, it won't be cached. Every request to that endpoint will load the endpoint rather than return a cached response. – neilco Nov 08 '13 at 10:19
  • 1
    Caching is supported if header includes options Cache-Control (max-age or s-maxage) OR Expires – HotJard Nov 10 '13 at 12:26