Associate each enum case with a type in Swift

106 Views Asked by At

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!

1

There are 1 best solutions below

2
JeremyP On BEST ANSWER

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