Lightweight persistence in iOS shouldn’t be that hard

Introducing Shallows — a reusable, easy-to-use caching library

I think you would agree that sometimes in iOS things that should be easy are made hard. Okay, not hard, but cumbersome, unintuitive, and absolutely no fun.

One of the good examples is a lightweight persistence. Maybe you want to save a small amount of user data on disk. Or to cache some information that came from the web. Or even to store images. You know that these things are kinda easy to implement, but you rather not.

Because these things are tricky. You’re going to write a lot of code ( LightweightPersistenceManager, anyone?), and there are a lot of things you can do wrong, and your solution will probably not be that good or that reusable.

That’s not your fault.

It’s just that on iOS, lightweight persistence doesn’t have its own level of abstraction. The system solutions lacks uniformity, flexibility and they tend to expose a lot of implementation details to us, sometimes making the code that depends on it untestable.

The goal of Shallows is to provide that missing level of abstraction.

First, here’s the link to a GitHub repo:

And now let’s dive in.

An abstraction

Meet Storage<Key, Value> — your main friend in a world of lightweight persistence with Shallows.

Storage instances are completely abstract — they don’t expose any implementation detail apart from the types of keys and values. Storages can perform “retrieve” and “set” operations, which are both fallible and asynchronous. Let’s take a look at them:

storage.retrieve(forKey: "batman") { (result) in
switch result {
case .success(let value):
// do something with a value
case .failure(let error):
// handle an error
}
}
storage.set(gordonImage, forKey: "gordon") { (result) in
if result.isSuccess {
// successful set
}
}

How can we get a Storage instance? Storage objects are type-erased, they don’t actually contain any logic. But Shallows provide these storages out of the box:

  1. MemoryStorage<Key: Hashable, Value>
  2. NSCacheStorage<Key: NSObject, Value: AnyObject>
  3. DiskStorage, which is a Storage<Filename, Data>

The last one is probably the most interesting. It is, in fact, our lightweight storage solution. Making one is a straightforward task:

let diskStorage = DiskStorage.folder("json-cache", in: .cachesDirectory)

But it’s just a storage with Data value, why is it interesting to us? Because the coolest thing about Storage is that its representation can be transformed in a way you need. So, for example, we can create a storage of JSON objects:

let jsonStorage = diskStorage.mapJSONDictionary() // Storage<Filename, [String : Any]>

Or use the magic of Swift 4 Codable:

struct Mail: Codable {
let text: String
let address: String
}
let mailDiskStorage = diskStorage.mapJSONObject(Mail.self) // Storage<Filename, Mail>

.mapJSONDictionary and .mapJSONObject are extensions of storage objects with Data value (as also a bunch of others), but we can also perform our custom transformations. For example, let’s create a disk image cache:

let diskStorage = DiskStorage.folder("image-cache", in: .cachesDirectory)
let imagesCache = diskStorage.mapValues(transformIn: { try UIImage(data: $0).unwrap() }, transformOut: { try UIImageJPEGRepresentation($0, 1.0).unwrap() }) // Storage<Filename, UIImage>

Now we have a completely functional disk cache of images. Did you notice how simple, and yet transparent that was? You control everything — you know exactly where your images will be stored and how, and yet it’s that easy. In fact, let’s extract our transformation to an extension, so it can be reused:

extension StorageProtocol where Value == Data {

func mapImages() -> Storage<Key, UIImage> {
return self.mapValues(transformIn: { try UIImage(data: $0).unwrap() }, transformOut: { try UIImageJPEGRepresentation($0, 1.0).unwrap() })
}

}

Now we have a ridiculously simple setup:

let imagesCache = DiskStorage.folder("image-cache", in: .cachesDirectory).mapImages()

And that’s it! Now with a single line of code we can create a disk-based image cache anywhere in our program. And in the end we get a Storage<String, UIImage> instance — completely abstract.

Next step

Shallows, despite being a really compact library, has a lot of powerful features. Probably, one of the best is storage composition.

The image cache we made above is not the most efficient one, because it’s gonna hit the disk every time we request an image. So wouldn’t it be nice to cache already retrieved images to memory? This logic, which could be a nightmare to implement yourself, is provided by Shallows with just a couple of lines of code:

let memoryCache = MemoryStorage<String, UIImage>()
let combinedCache = memoryCache.combined(with: imagesCache)

So now we have a two-layered (memory and disk) image cache. And, again, you’re in a full control of how your layers interact. Many caching libraries hides this from you — they make you completely ignorant about how it works, where it puts the files and so on. But Shallows lets you express your logic clearly, while still being super easy to use.


For now, you just saw how easy it is to create a lightweight storage, and how to make it fast and efficient. We will explore some other cool features and use-cases in upcoming posts, so stay tuned!

If you have any questions about Shallows (or about anything else) — ask me a question in “Responses” section below, or ping me on Twitter. Or you can also open an issue (or a pull request!) on GitHub:

As always, thanks for reading!


Hi! I’m Oleg, the author of Women’s Football 2017 and an independent iOS/watchOS developer with a huge passion for Swift. While I’m in the process of delivering my next app, you can check out my last project called “The Cleaning App” — a small utility that will help you track your cleaning routines. Thanks for your support!