Episode #306

Codable as a Caching Layer

19 minutes
Published on October 16, 2017

This video is only available to subscribers. Get access to this video and 572 others.

In a recent project I leveraged the Codable protocol to save API responses to disk to make the application more responsive (and to have an offline mode). I was happy with the results. In this episode we will add some caching to an existing application, saving JSON responses to the caches directory on the device.

Setting up storage options

We'll want to be able to easily switch the location of the stored files. We'll start by creating an enum to describe the types
of data we will store:

enum StorageType {
    case cache
    case permanent

    var searchPathDirectory: FileManager.SearchPathDirectory {
        switch self {
        case .cache: return .cachesDirectory
        case .permanent: return .documentDirectory
        }
    }

    var folder: URL {
        let path = NSSearchPathForDirectoriesInDomains(searchPathDirectory, .userDomainMask, true).first!
        let subfolder = "com.nsscreencast.TopRepos.json_storage"
        return URL(fileURLWithPath: path).appendingPathComponent(subfolder)
    }

    func clearStorage() {
        try? FileManager.default.removeItem(at: folder)
    }
}

This will allow us to specify that we want data to be stored in the Caches folder, allowing the OS to clean it up as it sees fit. If you have data that needs to be durable (say, like profile information for the currently logged in user), then you can use the Documents folder.

Saving / Loading Data on Disk

Next we'll create our storage class. We'll call it LocalJSONStore. It will be generic over the type of data we wish to save, so we will need to make sure that the generic type is Codable:

class LocalJSONStore<T> where T : Codable {
    let storageType: StorageType
    let filename: String

    init(storageType: StorageType, filename: String) {
        self.storageType = storageType
        self.filename = filename
        ensureFolderExists()
    }

    // ... snip   
}

Saving an object

    func save(_ object: T) {
        do {
            let data = try JSONEncoder().encode(object)
            try data.write(to: fileURL)
        } catch let e {
            print("ERROR: \(e)")
        }
    }

Retrieving a saved object

    var storedValue: T? {
        guard FileManager.default.fileExists(atPath: fileURL.path) else {
            return nil
        }
        do {
            let data = try Data(contentsOf: fileURL)
            let jsonDecoder = JSONDecoder()
            return try jsonDecoder.decode(T.self, from: data)
        } catch let e {
            print("ERROR: \(e)")
            return nil
        }
    }

It is important to note that errors are to be expected here. The data may become outdated as the application is updated, and so it may fail to decode back into your object. If this happens it is okay because we just won't have cached data, and the application will have to fetch and save a new copy.

This episode uses Xcode 9.1-beta1, Swift 4.