Search code examples
iosswiftnsurlcache

URLCache (iOS). storeCachedResponse works asynchronously. How to catch the completion?


Just discovered that the function storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) works asynchronously. That is, the result is not returned immediately after execution. I did not find a description of this in the official documentation. See example:

cache = URLCache(memoryCapacity: 0, diskCapacity: 100 * 1024 * 1024, diskPath: "myCache")
let config = URLSessionConfiguration.default
config.requestCachePolicy = .returnCacheDataElseLoad
config.urlCache = cache
let session = URLSession(configuration: config)
session.dataTask(with: request, completionHandler: {[unowned self]
    (data, response, error) in

    if let data = data, let response = response, ((response as HTTPURLResponse)?.statusCode ?? 500) < 300 {
        let cachedData = CachedURLResponse(response: response, data: data)                                            
        self.cache.storeCachedResponse(cachedData, for: request)
        let testCachedData = self.cache.cachedResponse(for: request)
    }
}

Theoretically testCachedData must contain cached response. But what it actually contains:

testCachedData?.response.url // Ok
testCachedData?.isEmpty // false
testCachedData?.data // 0 bytes!!!

Although testCachedData?.data says it contains 0 bytes, we can write this data to a file, and this file will contain real data, not 0. If we deep into local cache directory (~/Library/Caches/myApp/MyCache) when pausing at breakpoint right after cachedResponse call, we can see that folder with cached files (fsCachedData) doesn't exist yet. Now let's insert delay between storeCachedResponse and cachedResponse:

cache = URLCache(memoryCapacity: 0, diskCapacity: 100 * 1024 * 1024, diskPath: "myCache")
let config = URLSessionConfiguration.default
config.requestCachePolicy = .returnCacheDataElseLoad
config.urlCache = cache
let session = URLSession(configuration: config)
session.dataTask(with: request, completionHandler: {[unowned self]
    (data, response, error) in

    if let data = data, let response = response, ((response as HTTPURLResponse)?.statusCode ?? 500) < 300 {
        let cachedData = CachedURLResponse(response: response, data: data)                                            
        self.cache.storeCachedResponse(cachedData, for: request)
        delay(5) // JUST 5 SEC DELAY
        let testCachedData = self.cache.cachedResponse(for: request)
    }
}

Now:

testCachedData?.response.url // Ok
testCachedData?.isEmpty // false
testCachedData?.data // contains bytes

So, after 5 sec delay we see that cached files folder (fsCachedData) exists and contains cached file (e.g. D8A30D21-C8F1-4FCA-967E-F6B440998173).

The point is how to catch the completion of storeCachedResponse?

I'm going to use cached files right after they are created. Moreover, I'm going to handle cached files directly, and it's not the best solution to set delay.


Solution

  • Actually I couldn't understand why you calling cached data immediately after caching!? In my opinion you should call cached data before requesting url with session if data is exist return cached data else request from the scratch.

    For example :

    private let allowedDiskSize = 100 * 1024 * 1024
    private lazy var cache: URLCache = {
        return URLCache(memoryCapacity: 0, diskCapacity: allowedDiskSize, diskPath: "gifCache")
    }()
    
    typealias DownloadCompletionHandler = (Result<Data,Error>) -> ()
    
    private func createAndRetrieveURLSession() -> URLSession {
        let sessionConfiguration = URLSessionConfiguration.default
        sessionConfiguration.requestCachePolicy = .returnCacheDataElseLoad
        sessionConfiguration.urlCache = cache
        return URLSession(configuration: sessionConfiguration)
    }
    
    private func downloadContent(fromUrlString: String, completionHandler: @escaping DownloadCompletionHandler) {
    
        guard let downloadUrl = URL(string: fromUrlString) else { return }
        let urlRequest = URLRequest(url: downloadUrl)
        // First try to fetching cached data if exist
        if let cachedData = self.cache.cachedResponse(for: urlRequest) {
            print("Cached data in bytes:", cachedData.data)
            completionHandler(.success(cachedData.data))
    
        } else {
            // No cached data, download content than cache the data
            createAndRetrieveURLSession().dataTask(with: urlRequest) { (data, response, error) in
    
                if let error = error {
                    completionHandler(.failure(error))
                } else {
    
                    let cachedData = CachedURLResponse(response: response!, data: data!)
                    self.cache.storeCachedResponse(cachedData, for: urlRequest)
    
                    completionHandler(.success(data!))
                }
            }.resume()
        }
    }
    

    And usage:

    self.downloadContent(fromUrlString: ANY_URL, completionHandler: { (result) in
    
                switch result {
                case .success(let yourData):
                    // handle data
    
                case .failure(let error):
                    debugPrint(error.localizedDescription)
                }
     })
    

    First time it will fetch data from the web and in second request it will return cached data immediately.