0

In my app, I retrieve url data from the server. As an optimization, I normalize the data by storing it into a custom class so that the data can be quickly accessed later on. However, I need to store an instance of this class locally and offline so that I can avoid retrieving data from the server if I've already done so. Also, the website changes daily, so the cached data is only useful if it's from the same day.

This scenario applies to 2 separate classes (4 objects in total), and I've tried saving this data with NSCoding like so. (I've only tried implementing 1 object so far, but I'm planning on creating a separate docPath for each object)

- (void) saveStateToDocumentNamed:(NSString*)docName
{
    NSError       *error;
    NSFileManager *fileMan = [NSFileManager defaultManager];
    NSArray       *paths   = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString      *docPath = [paths[0] stringByAppendingPathComponent:docName];

    if ([fileMan fileExistsAtPath:docPath])
        [fileMan removeItemAtPath:docPath error:&error];

    // Store the hours data
    Hours *h = [self parseHoursFromServer];
    NSDictionary *state = [NSDictionary dictionaryWithObjectsAndKeys:h, @"Hours", nil];

    // There are many ways to write the state to a file. This is the simplest
    // but lacks error checking and recovery options.
    [NSKeyedArchiver archiveRootObject:state toFile:docPath];
    NSLog(@"end");
}

- (NSDictionary*) stateFromDocumentNamed:(NSString*)docName
{
    NSError       *error;
    NSFileManager *fileMan = [NSFileManager defaultManager];
    NSArray       *paths   = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString      *docPath = [paths[0] stringByAppendingPathComponent:docName];

    if ([fileMan fileExistsAtPath:docPath])
        return [NSKeyedUnarchiver unarchiveObjectWithFile:docPath];

    return nil;
}

However, when I tried running this code, I received a [Hours encodeWithCoder:]: unrecognized selector sent to instance 0 because my class currently does not support NSCoding. After reading about how I need to manually encode all the properties for my custom class, I want to make sure that NSCoding is an ideal solution to my data caching problem.

EDIT:

Here is how I currently create the url

NSURL *url = [NSURL URLWithString:urlString];
NSData *pageData = [NSData dataWithContentsOfURL:url];
Mahir
  • 1,684
  • 5
  • 31
  • 59
  • You know it's already being cached, right? https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSURLCache_Class/Reference/Reference.html – quellish Jul 31 '14 at 03:45
  • And if you are caching something on the file system, use NSCachesDirectory not NSDocumentDirectory. It's also a good idea to create your own subdirectory within that directory. – quellish Jul 31 '14 at 03:48
  • Maybe I'm approaching this the wrong way, but I thought it would be better to store the custom class, not just the raw server data – Mahir Jul 31 '14 at 03:56
  • The process of extracting the data and passing it to the class is a significant amount of code, so I was hoping to eliminate this process as well through caching – Mahir Jul 31 '14 at 03:58
  • If I am not mistaken, you would still have to do that. If you just allow the URL loading system to do it's job, you don't have to implement and test NSCoding and a custom caching implementation on top of that. – quellish Jul 31 '14 at 04:00
  • I smell premature optimisation. how much time do you loose for reinstating the object from the cache? 1 nano second? 10? still too fast for your neurons. and reading from disk might me much slower. – vikingosegundo Jul 31 '14 at 04:03
  • @vikingosegundo So are you also in favor of caching the server data only and not the object? – Mahir Jul 31 '14 at 04:06
  • @quellish I had looked into NSURLCache earlier, but I wasn't sure if I would be able to check if the cached data is from the current date – Mahir Jul 31 '14 at 04:14
  • @Mahir I'd suggest you step back and identify what the purpose for your local store is. If it's just to cache, then the default cache mechanism might be fine (though it can be twitchy, i.e. it applies some poorly documented criteria to decide whether it will really cache or not, you'd have to make sure you're ok with server controlling caching). But if your local store is, for example, to show the user the last known state while the new network request is in progress, then I think it's a mistake to contort `NSURLCache` for this purpose and something like `NSCoding` makes perfect sense. – Rob Jul 31 '14 at 06:18

1 Answers1

1

TL;DL; The system is already caching for you!

When a request is made, the URL loading system will check the shared URL cache to see if there is a valid matching response in the cache. If there is, it will use that rather than making a network connection. This is transparent to your networking code, it works the same wether it's being given data from the cache or the network. You don't have to implement anything other than tell NSURLCache to use the disk:

NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:(4 * 1024 * 1024) diskCapacity:(20 * 1024 * 1024) diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];

That would allow it to use 4MB RAM, 20MB on disk, and the default cache location on disk. This should be done once, when your application starts up. From that point on, NSURLCache will write any cacheable network responses to memory as well as disk. As they expire or the cache runs out of space they will be removed. How the network connection uses the cache is determined by the NSURLRequest's cachePolicy. The default of NSURLRequestUseProtocolCachePolicy uses the cache in accordance with the rules of the protocol (HTTP). HTTP responses include information about how long a response should be considered valid for, and this policy obeys those rules. For the most part, this works well - unless the server you are communicating with implements HTTP response caching information incorrectly. If you want to alter the cache policy to NOT allow the URL loading system to attempt to load anything from the network, ONLY do so if the device's radios are offline. If the device is in airplane mode you would construct your NSURLRequest with a different cache policy (assuming you are using Reachability to determine network status):

if (networkStatus == NotReachable){
    request = [[NSURLRequest alloc] initWithURL:someURL cachePolicy: NSURLRequestReturnCacheDataDontLoad timeoutInterval:timeout];
} else {
    request = [[NSURLRequest alloc] initWithURL:someURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:timeout];
}

This would use the policy NSURLRequestReturnCacheDataDontLoad if the device was in airplane mode or neither cellular or wifi had a signal. NSURLRequestReturnCacheDataDontLoad will load responses from the cache (even if expired), and not attempt to access the network for items that are not in the cache.

You can read more about this in the URL Loading System Programming Guide: Understanding Cache Access

quellish
  • 21,123
  • 4
  • 76
  • 83
  • So it seems like I can determine to cache or not with `connection:willCacheResponse:`, but I'm still not sure how to check the date of the cached data. Would I just store the date in `NSUserDefaults` every time I refresh the cache? – Mahir Jul 31 '14 at 04:56
  • No, that method *tells* the delegate it will cache the response. For what you want, you should not implement that. Let NSURLCache do it's job as described in the answer. That is all you need to do. – quellish Jul 31 '14 at 06:35
  • So does it automatically check if the cached data is up-to-date? – Mahir Jul 31 '14 at 09:12
  • Yes, it does check to see if the cached response is valid. – quellish Jul 31 '14 at 16:57
  • I tried adding `NSURLCache *URLCache` as you described in your answer, but it's still making a call to the server instead of loading data from the cache. I added details to my question showing how I init the url -- also I'm wondering if caching is limited for iOS – Mahir Jul 31 '14 at 22:18
  • How do you know it's contacting the server (Charles, etc)? Is the server telling the client not to cache anything? – quellish Aug 01 '14 at 22:26
  • Sorry, I don't follow – Mahir Aug 02 '14 at 04:08