sunshinejr / SwiftyUserDefaults

Modern Swift API for NSUserDefaults
http://radex.io/swift/nsuserdefaults/static
MIT License
4.85k stars 365 forks source link

Is it possible to add some namespace with keys defined in DefaultsKeys? #248

Closed M-I-N closed 4 years ago

M-I-N commented 4 years ago

I would like to add context before any of the keys defined in the extension of DefaultsKeys. Like maybe I'll have some identical keys for different contexts. Say I want to enable/disable showing image in image views in different screens and I want to have option to toggle this setting from the application settings screen of my app.

For the context of this question, the simple scenario would be like using API as below:

Defaults.LandingScene.showImages = true
Defaults.FeedScene.showImages = true
Defaults.ChatScene.showImages = false

which would in turn become the keys as:

Is it easily achievable?

For anyone interested to know more of the background, take a look at this article.

sunshinejr commented 4 years ago

@M-I-N given that we just switched to use key paths and adapter/keystore, I think this should be easily achievable. I actually just tried this so here's what you would do.

  1. Create your own KeyStore:

    struct KeyStore: DefaultsKeyStore {
    struct LandingScenes {
        var showImages = DefaultsKey<Bool>("test", defaultValue: true)
    }
    
    let landingScenes = LandingScenes()
    }
  2. Create your own defaults:

    let keyStore = KeyStore()
    var defaults = DefaultsAdapter(defaults: .standard, keyStore: keyStore)
  3. Use:

    defaults[\.landingScenes.showImages] = false

I tested the dynamicKeyPath syntax but it doesn't seem to work, would need to investigate more as of why.

M-I-N commented 4 years ago

@sunshinejr thanks for your prompt response. Looks like you may have missed one consideration. I am not only concerned about the usage of API but also about the key that will actually be used by the UserDefaults. The solution you suggested is considering the usage of the API only.

Like:

defaults[\.landingScenes.showImages] = false
defaults[\.feedScenes.showImages] = true

the statements are using namespace at the usage. But under the hood, the actual keys aren’t aware of the namespacing. And as developers if we aren’t careful enough we might end into duplicate key names used by more than one struct.

Well, to be honest, I tried to construct a solution before posting this issue here. I was stuck at a point where I could not think of having a property for each of the struct types defined inside the KeyStore. But your suggestion resolves that. I’m posting my solution in the below comment. Have a look at your free time and maybe you can consider supporting this right in the module itself.

M-I-N commented 4 years ago

First we declare a protocol which will take a key and transform it to a namespaced key.

protocol UserDefaultKeyNamespacing {
    associatedtype DefaultKey: RawRepresentable
    func nameSpacedKey(for defaultKey: DefaultKey) -> String
}

extension UserDefaultKeyNamespacing where DefaultKey.RawValue == String {
    func nameSpacedKey(for defaultKey: DefaultKey) -> String {
        return "\(Self.self).\(defaultKey.rawValue)"
    }
}

Here, maybe we could think of something like CodingKeys from the Codable protocol where every property introduction needs an entry in the case of the enum. But I’m not into that road yet.

Then we will declare extension on DefaultsKeys instead of defining new key store. New custom key store will work just fine. We will define our required structs here by conforming them to the protocol we declared earlier. And we will construct our DefaultsKey with the namespaced key.

extension DefaultsKeys {

    struct LandingScene: UserDefaultKeyNamespacing {
        enum DefaultKey: String {
            case showImage
        }

        var showImage: DefaultsKey<Bool> {
            let key = nameSpacedKey(for: .showImage)
            return .init(key, defaultValue: true)
        }

    }

    struct FeedScene: UserDefaultKeyNamespacing {
        enum DefaultKey: String {
            case showImage
        }

        var showImage: DefaultsKey<Bool> {
            let key = nameSpacedKey(for: .showImage)
            return .init(key, defaultValue: true)
        }
    }

    var landingScene: LandingScene {
        return LandingScene()
    }

    var feedScene: FeedScene {
        return FeedScene()
    }

}

Finally the usage will be:

Defaults[\.landingScene.showImage] = true
Defaults[\.feedScene.showImage].toggle()

As @sunshinejr pointed out the issue with dynamicKeyPath syntax, it is yet to be resolved.

sunshinejr commented 4 years ago

awesome, glad you got it to work! closing this for now then, but let me know if you have any other questions regarding this one!