iOS - How does caching work? - ios

I am working on an app representing JSON from web API.
The remote source updates several times a day.
All I want to do is:
// pseudo code
makeRequest() {
if (network not available){
if (cache not exists) {
showEmptyScreen()
}else if (cache exists){
useCache()
}
}else if (network available){
if (cache not exists) {
loadFromRemote()
}else if (cache exists){
if (cache is older than latest update) {
loadFromrRemote()
}else {
useCache()
}
}
}
}
I've read about NSURLCache from Apple and from NSHipster.
It's still confusing that if NSURLCache could do what I want. For instance, how does it work to check if there's a newer version of data without really download anything?
And if NSURLCache could not handle the checking, how could I code it myself?
Thanks for any advice!

How caching behaves is mostly up to you and your server. NSURLCache does not make decisions on its own other than perhaps what to do when its capacity limit is exceeded.
When you are making a request, headers like If-Modified-Since will determine if data is transferred or not by comparing the timestamps of the cached data.
Server based headers such as Cache-Control can also affect how long data remains valid in the cache.
In summary, NSURLCache can handle what you need but the implementation will be based on a combination of the configuration of NSURLCache, how you make requests, how cache control is implemented in responses and whether you override the policies given by the headers.

Related

Bypass Service-Worker caching

I have a progressive web-app, which speaks to an API. The calls to this api get cached by a service worker, which works great.
But now, I want to add a reload-button, which ideally forces the service worker to try to bypass the cache and update it if successful, also it should not return the cached result if a connection could not be made.
I am a bit unsure how to solve this. I am using the sw-toolbox.
All requests go through the fetch callback which receives a request object. Thus, before returning a cached response you can look for an additional header parameter (you need to include it into your request to API) to skip the logic returning cached response.
Based on your description, you are using the application cache. It can be accessed from the app fronted independent of the sw-tool box.
function onReloadButtonClicked(event) {
//Check for browser cache support
if ('caches' in window) {
//Update cache if network query is successful
caches.open('your_cache_name')
.then(function(cache) {
cache.add('your_url');
}).catch(function(err) {
// Do something with the error
});
}
}

What is the better way to encrypt NSURLCache?

I want to encrypt/decrypt all cached data from a NSURLSession using AES256. I'm new using Alamofire but I think it is possible to do it without involving the library itself.
I don't know exactly what is the most seamless way to encrypt the data before caching and decrypt it after being retrieved from cache.
I see I can use Alamofire's SessionDelegate and the methods dataTaskWillCacheResponse and dataTaskWillCacheResponseWithCompletion to encrypt but I don't see anything related with the data being extracted from the cache to do the decrypting.
On the other hand I was thinking about a custom NSURLProtocol to override cachedResponse but I don't see anything related with the caching of that response, only with the extracted data.
In summary, I don't know if it is possible to accomplish this, or I have to use a mix between the NSURLSessionDelegate/SessionDelegate and NSURLProtocol, or maybe subclass NSURLCache to do the job and pass it to the Alamofire session, or there is something simpler out there, or I'm terribly wrong :P
Any help will be really appreciated.
EDIT
I'm trying to achieve it with the next implementation. First of all a very simple subclass of the cache:
class EncryptedURLCache: URLCache {
let encryptionKey: String
init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String? = nil, encryptionKey: String) {
guard !encryptionKey.isEmpty else {
fatalError("No encryption key provided")
}
self.encryptionKey = encryptionKey
super.init(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: path)
}
override func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
return super.cachedResponse(for: request)?.cloneDecryptingData(withKey: encryptionKey)
}
override func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
super.storeCachedResponse(cachedResponse.cloneEncryptingData(withKey: encryptionKey), for: request)
}
}
And an extension of the cached response to return the encrypted/decrypted data
extension CachedURLResponse {
func cloneEncryptingData(withKey key: String) -> CachedURLResponse {
return clone(withData: data.aes256Encrypted(withKey: key))
}
func cloneDecryptingData(withKey key: String) -> CachedURLResponse {
return clone(withData: data.aes256Decrypted(withKey: key) ?? data)
}
private func clone(withData data: Data) -> CachedURLResponse {
return CachedURLResponse(
response: response,
data: data,
userInfo: userInfo,
storagePolicy: storagePolicy
)
}
}
This is working but only for a mockable.io that I mounted with the header Cache-Control: max-age=60. I'm also testing against the SWAPI http://swapi.co/api/people/1/ and against Google Books https://www.googleapis.com/books/v1/volumes?q=swift+programming.
In all three cases the responses are correctly encrypted and cached. I'm doing my testing cutting off the Internet connection and setting the session configuration's requestCachePolicy = .returnCacheDataDontLoad.
In this scenario, the request made to mockable.io is correctly decrypted and returned from cache but the others say NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline.". This is VERY strange because, with that policy, it has to say NSURLErrorDomain Code=-1008 "resource unavailable" if there is no possibility to return the cached data. If there is an error decrypting then it says it was an error serializing to a JSON object.
I've also tested with the common shared cache and it works as expected, with that policy the data is returned. I thought it could be something related with the absence of cache headers in the SWAPI and GBooks responses but this test works, it returns the cached data.
Then I made another test: using my cache but without encrypting/decrypting data, simply cloning the returned cached response with the data as is, with no results. Then I tried a final and very stupid test: to avoid cloning the response, just return the cachedResponse and then IT WORKED. How the h*** is that possible? If I clone the cachedResponse to inject my encrypted/decrypted data it does not work! Even in examples from Apple they are creating new cached responses with no fear.
I don't know where is the error but I'm going to jump over the window in a minute or two.
Please, any help? Thank you so much.
EDIT 2
I was changing emails with a DTS engineer from Apple and the conclusion is that this is not possible to achieve this because the backing CF type is doing more logic than the Foundation object, in this case it is doing a validation against the URLRequest that is passed to it when the system caches the response, but I cannot pass it when make the clone with the regular NSCachedURLResponse.
When the system validates against the request, there is none to match with.
There is no way to intercept cache retrieval calls from the delegate side that I'm aware of, and I don't think that a custom protocol will even be asked to handle the request if it comes out of the cache, but I could be wrong. So probably your options are:
Explicitly ask the cache for the data before you make the URL request.
Add code in the code that actually handles the response so that it recognizes that the data is encrypted and decrypt it.
For example, you could insert an additional header into the headers as you store it into the cache to indicate that the cached data is encrypted. Then, when you see that magic header value on the way back out, decrypt it.
Write a subclass of NSURLCache and handle the decryption there (and ideally, store the on-disk data in a different file to avoid breaking any requests in your app that use the normal cache).

UIWebView slow loading using NSUrlProtocol Xamarin.Forms

I'm working on an iOS application using Xamarin.Forms. This application is using UIWebView controller that shows a web application that is hosting on my server. Each time that I make a request I have to send a custom header in order to identify that this request comes to the mobile application and not from a browser, to do this I'm using an NSUrlProtocol object that overrides the method Request that inserts the custom header on each request.This is my code:
public override NSUrlRequest Request {
get {
NSMutableDictionary headers = null;
if (null == base.Request.Headers) {
headers = new NSMutableDictionary ();
} else {
headers = new NSMutableDictionary (base.Request.Headers);
}
headers.Add(NSObject.FromObject(AppVariables.headerVariable), NSObject.FromObject (AppVariables.appVersion));
NSMutableUrlRequest newRequest = (NSMutableUrlRequest)base.Request.MutableCopy ();
newRequest.Headers = headers;
return newRequest;
}
}
The problem that I have right now is that I noticed since I started using the NSUrlProtocol the loading time of the pages is increasing a lot. Right now the loading is taking 10 seconds, before this implementation the page took 3 seconds approximately.
Can anyone please point out some helpful direction to overcome this??
I don't see any reasons for the delay in response time when you're using custom headers. Like Andreas mentioned in the comments, I believe it has to do with your server code. I would recommend profiling your server code.
Do you see similar results when you send the requests (with custom headers) from Fiddler or cURL?
Just like #AndreasPaulsson and #prashant had mentioned, server might be the culprit. I would recommend testing the API with tools like Postman and check the response speed. I would also recommend you to check ModernHttpClient by Paul C Betts. In iOS the library uses NSUrlSession.

How does - [NSURLCache cachedResponseForRequest:] actually get the matching cache?

How does -[NSURLCache cachedResponseForRequest:] works? Is it works like key-value pair where the key is the content of the NSURLRequest object (which includes the NSURL object, cache policy, time out interval, HTTPBody, HTTPHeaders)? If so then if any of the above field is different then we could not retrieve the cache.
For example, assuming the NSURLRequest has everything staying the same except the HTTPBody, which one request has the body of:
{
a = 1;
}
while another request has the body of:
{
a = 2;
}
Am I be able to get the cache of former request using the latter request?
IIRC, only the URL is taken into account. If header or body data dfferences matter to you, you'll need to implement a custom cache. The URL Loading System Programming guide should be of some help, should you decide to go down that path.

RestKit: How to reject an entire mapping when a value is not valid

So I'm trying to support offline usage of an iOS application I'm making that uses a REST API. Here's what I have so far:
A server running with a REST interface to manipulate my data model.
An iOS application that uses RestKit to retrieve the data stored on my server.
RestKit stores server responses locally in Core Data.
When the server is unavailable I still want users to be able to update the data model and then when the server becomes available again I want those updates to be pushed to the server.
The issue I've run into is that when a value that has been updated locally (but not pushed to the server yet) is received from the server it is overwritten with the contents of the server's response. To prevent this I am trying to cancel the save to my local storage if the new 'updatedAt' date is before the current 'updatedAt' date.
This is my current validation function:
- (BOOL)validateUpdatedAt:(id *)ioValue error:(NSError **)outError
{
if(self.updatedAt && [self.updatedAt compare:((NSDate *)*ioValue)] == NSOrderedDescending) {
*outError = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorMappingDeclined userInfo:nil];
return NO;
}
return YES;
}
This works, but only prevents this individual value from being changed. I want the entire update of that object to be canceled if this one field is invalid. How do I do this?
The support available from RestKit is to set discardsInvalidObjectsOnInsert for the whole object to be discarded when validation fails. But, this won't work for you as it only works for NSManagedObject instances that are being inserted, because it uses validateForInsert:.
You could look at using validateForUpdate: to perform a similar check and then reverting the changes, but RestKit isn't really offering you anything in terms of the abort in your case.

Resources