I'm trying to apply the dependency inversion principle to the UserDefaults and also make it as strong-typed as possible.
Suppose that I have the following enum:
enum LocalStorageKeys: String {
case user
case account
}
Then, I need a LocalStorage
protocol:
protocol LocalStorage {
func save(key: LocalStorageKeys, value: Any) throws
func get(key: LocalStorageKeys) -> Any?
}
I know that we could use a generic both in save and get methods, but how could I implement it in a way that when using localStorage.save(key: .user, value: user)
, it must be provided with the User
, and only with the User
type? Also, how could I do so the compiler can infer that localStorage.get(.user)
is of type User
?
In the end, I would like to protect myself from doing things like localStorage.save(key: .user, value: someOtherThing)
Thanks!
My solution based on how Apple does its serialisation is to have different key enumerations per type you want to be able to save.
First create a protocol that anything that can be saved (or got back) must adopt
protocol Saveable
{
associatedtype Key: CodingKey
}
Any type that you want to be able to save must have a Key
nested type that conforms to CodingKey
. The latter is only so that I have a consistent way to extract a key as a string from an instance of Key
.
Change your protocol as follows
protocol LocalStorage
{
func save<T: Saveable>(value: T, for key: T.Key)
func get<T: Saveable>(type: T.Type, key: T.Key) -> T?
}
If T.Key
is an enum, whenever you save a value of type T
you are automatically restricted to using the enum cases from T.Key
. The reason I put value
first is because it means that when you are typing a call to the function, by the time you get to for:
the Xcode editor has already figured out what the allowable keys are and will give you a completion list to choose from.
Here is how you might implement User
struct User
{
var name: String
}
extension User: Saveable
{
enum Key: String, CodingKey
{
case user
case otherUser
}
}
In the above case, there are two allowed keys for storing a User
.
And here is a stub implementation of a LocalStorage
struct StorageImplementation: LocalStorage
{
func save<T: Saveable>(value: T, for key: T.Key)
{
print(key.stringValue)
}
func get<T: Saveable>(type: T.Type, key: T.Key) -> T?
{
print(key.stringValue)
return nil
}
}
And this is what calling it looks like
let user = User(name: "jeremy")
StorageImplementation().save(value: user, for: .otherUser) // prints otherUser
StorageImplementation().get(type: User.self, key: .user) // prints user