A datastore for iOS that uses file or the keychain

Tom Zurkan
5 min readJul 14, 2022

There is almost always a need for some sort of data store or persistent storage implementation for your app or SDK. We have a lot of options when it comes to storing data on our iOS device (UserDefaults, SQLite, File, Core Data, Keychain, etc.). In this article we are going to discuss file based persistent storage and the keychain.

First, let’s create our protocol for the DataStore:

protocol DataStore {
associatedtype T : Codable
func getItem(forKey: String) -> T?
func removeItem(forKey: String)
func putItem(forKey: String, item: T)
}

The above is a simple datastore that supports codeable data types. It has a simple getItem, removeItem, and putItem. There is only one item type per datastore.

I found that in order to include the datastore into another class, I needed to create a default generic implementation in order to define the type alias and include that as the super class that any other generics implementing would derive from:

import Foundationclass DataStoreImpl<Item : Codable> : DataStore {
typealias T = Item
func getItem(forKey: String) -> T? {
return nil
}
func removeItem(forKey: String) {}
func addItem(forKey: String, item: T) {}
}

This generic implementation class allowed me to create other generics classes from it for different implementations that can be included in another class. So, in the following:

class SomeOtherClass {
storage:DataStore
}

The above would not compile because it does not know the type alias. Instead, we need to define it like the following:

class SomeOtherClass<Item:Codable> {
storage:DataStoreImpl<Item>
}

Now, I have some other class like a queue that could define an item to hold and the list to hold them in like the following:

class DataQueueImpl<Item: Codable> : DataQueue {
let dataStore:DataStoreImpl<[Item]>
let name:String
let lock:DispatchQueue
let queueSize: Int
// pass in datastore to use at initialization
typealias T = Item
}

From there, I can write a simple file datastore as an example implementation:

import Foundationclass DataStoreFile<Item: Codable> : DataStoreImpl<Item> {
var lock: DispatchQueue
var url:URL
init(name:String) {
lock = DispatchQueue(label: name, qos: .default)
#if os(tvOS) || os(macOS)
let directory = FileManager.SearchPathDirectory.cachesDirectory
#else
let directory = FileManager.SearchPathDirectory.documentDirectory
#endif
if let url = FileManager.default.urls(for: directory, in: .userDomainMask).first {
self.url = url.appendingPathComponent(name, isDirectory: false)
} else {
self.url = URL(fileURLWithPath: name)
}
}
override func getItem(forKey: String) -> Item? {
var item:Item?
lock.sync {
guard let data = try? Data(contentsOf: self.url) else {
return
}
let dict = try? JSONDecoder().decode(Dictionary<String,Item>.self, from: data)
item = dict?[forKey]
}
return item
}
override func removeItem(forKey: String) {
lock.async {
guard let data = try? Data(contentsOf: self.url) else {
return
}
var dict = try? JSONDecoder().decode(Dictionary<String,Item>.self, from: data)
dict?.removeValue(forKey: forKey)
if dict != nil, let d = try? JSONEncoder().encode(dict!) {
try? self.write(data: d)
}
}
}
private func write(data d:Data) throws {
try d.write(to: self.url, options: [.atomic, .completeFileProtection])
}
override func putItem(forKey: String, item: Item) {
lock.async {
guard let data = try? Data(contentsOf: self.url) else {
let d = [forKey: [item]]
if let data = try? JSONEncoder().encode(d) {
try? self.write(data: data)
}
return
}
var dict = try? JSONDecoder().decode(Dictionary<String,Item>.self, from: data)
if dict == nil {
dict = [forKey: item]
}
dict?[forKey] = item
if dict != nil, let d = try? JSONEncoder().encode(dict!) {
try? self.write(data: d)
}
}
}
typealias T = Item
}

In the above implementation, we use a dictionary to hold any number of items by name although they must be the same type. The item must support codable so we can convert it to JSON data. The dictionary is backed by a file. The item held could be a list of items such as described above in the queue or a single item.

Finally, I wanted to discuss using the iOS keychain. The file store above uses completeFileProtection. That means that the file is encrypted and should only be readable by the application. But, sometimes, you might want something a little more secure than that. Now, we have the keychain. The keychain is a system wide keychain where you can store encrypted items. Below is an implementation of the keychain datastore:

import Foundation
import Security
class DataStoreKeyStore<Item: Codable> : DataStoreImpl<Item> {
let service : String
var keychainHolders = [String: KeychainPasswordItem]()
init(key:String) {
service = key
do {
let khs = try KeychainPasswordItem.passwordItems(forService: service)
for k in khs {
keychainHolders[k.account] = k
}
}
catch(let e) {
print(e)
}
}
override func getItem(forKey: String) -> Item? {
if let kh = keychainHolders[forKey] {
if let key = try? kh.readPassword() {
return try? JSONDecoder().decode(Item.self, from: key)
}
}
return nil
}
override func removeItem(forKey: String) {
if let kh = keychainHolders[forKey] {
do{
try kh.deleteItem()
keychainHolders.removeValue(forKey: forKey)
}
catch {
print(error)
}
}
}
override func putItem(forKey: String, item: Item) {
let kh = KeychainPasswordItem(service: service, account: forKey, accessGroup: nil)
removeItem(forKey: forKey)
do {
let data = try JSONEncoder().encode(item)
try kh.savePassword(data)
keychainHolders[forKey] = kh
}
catch {
print(error)
}
}
typealias T = Item
}

The above uses the keychain to store the item as a password string because it is an encoded Data object. The KeyChainPasswordItem is based on the example from apple for generic keychain storage. But I adapted it so as not to go string to data. It uses the key chain to add the object as a password byte array to the keychain.

Again, I wrote this article because it was hard to find how to save a generic item to the keychain. As you can see it is pretty straightforward. The file with complete protection might be enough for most uses. But, if you want to provide some extra level of security, then the keychain is a good alternative. Keep in mind that you really don’t want to store any sensitive data on the device. But, any data could be considered sensitive. With that said, even storing simple items might be better served to be encrypted.

I hope this article, which is pretty thick in code, helps developers to better understand how to implement a data store using either file or keychain.

--

--

Tom Zurkan
Tom Zurkan

Written by Tom Zurkan

Engineer working on full stack. His interests lie in modern languages and how best to develop elegant crash proof code.

No responses yet