Skip to content
Zhiyu Zhu/朱智语 edited this page May 16, 2022 · 9 revisions

As @propertyWrapper

@EFStorageUserDefaults(forKey: "someKey")
var someValueToBeStoredInUserDefaults: String = "Default Value"

Through @dynamicMemberLookup

let someReference: EFStorageUserDefaultsRef<String> = UserDefaults.efStorage.someKey

this is equivalent to

let someReference = EFStorageUserDefaultsRef<String>.forKey("someKey")

and you can access the value stored in UserDefaults.standard by

someReference.content

which can be simplified as

UserDefaults.efStorage.someKey as String?

and assign the content through

UserDefaults.efStorage.someKey = "OwO"

Non-default Container

Should you need to use a different instance, you can do that too

@EFStorageUserDefaults(forKey: "anotherKey", 
                       in: UserDefaults.standard, 
                       persistDefaultContent: true)
var inAnotherStorage: Bool = true

UserDefaults.standard.efStorage.anotherKey // either content or the reference to it

EFStorageUserDefaultsRef<Bool>.forKey("anotherKey", in: UserDefaults.standard)

Or, if you want to replace the default container for all, try this:

extension UserDefaults {
    private static let appGroup = UserDefaults(
        suiteName: "you.user.defaults.group"
    )
    
    @_dynamicReplacement(for: makeDefault())
    class func makeDefaultForAppGroup() -> Self {
        return (UserDefaults.appGroup as? Self) ?? makeDefault()
    }
}

Supported Containers

As of now, we offer support for UserDefaults and Keychain (provided by KeychainAccess). You can combine them to form a new type of container, or to support previous keys.

@EFStorageComposition(
    EFStorageUserDefaults(wrappedValue: false, forKey: "isNewUser"),
    EFStorageKeychainAccess(wrappedValue: false, forKey: "isNewUser"))
var isNewUser: Bool

@SomeEFStorage(
    EFStorageKeychainAccess(wrappedValue: false, forKey: "paidBefore")
    + EFStorageUserDefaults(wrappedValue: false, forKey: "paidBefore")
    + EFStorageUserDefaults(wrappedValue: true,
                            forKey: "oldHasPaidBeforeKey",
                            persistDefaultContent: true))
var hasPaidBefore: Bool

To migrate from another data type, you can use a migrator

@EFStorageComposition(
    EFStorageUserDefaults<String>(wrappedValue: "Nah",
                                  forKey: "sameKey"),
    EFStorageMigrate(
        from: EFStorageUserDefaults<Int>(
            wrappedValue: 1551,
            forKey: "sameKey",
            persistDefaultContent: true),
        by: { number in String(number) }
    )
)
var mixedType: String

More Storables

While EFStorage provides extension on common types you may want to store, there are times when you need to make some other types storable. You can look at the source code of EFStorage to see how we did it, and these are the protocols you may want to adopt:

KeychainAccess

public protocol KeychainAccessStorable {
    func asKeychainAccessStorable() -> Result<AsIsKeychainAccessStorable, Error>
    static func fromKeychain(_ keychain: Keychain, forKey key: String) -> Self?
}

Since KeychainAccess only supports String or Data, you have to convert it to one of those

public enum AsIsKeychainAccessStorable {
    case string(String)
    case data(Data)
}

UserDefaults

public protocol UserDefaultsStorable {
    func asUserDefaultsStorable() -> Result<AsIsUserDefaultsStorable, Error>
    static func fromUserDefaults(_ userDefaults: UserDefaults, forKey key: String) -> Self?
}

UserDefaults is interesting, because it takes Any. Every type that you might expect to save in UserDefaults directly has adopted the AsIsUserDefaultsStorable protocol, so don't conform to it unless you know what you are doing.

public protocol AsIsUserDefaultsStorable: UserDefaultsStorable { }

YYCache

public protocol YYCacheStorable {
    func asYYCacheStorable() -> Result<NSCoding, Error>
    static func fromYYCache(_ yyCache: YYCache, forKey key: String) -> Self?
}

YYCache only takes types conforming to NSCoding, so that's easy.

Assertion Failures

EFStorage traps through assertionFailure when types fail to convert to some storable, which means it only crashes for DEBUG and not for production (it just don't do anything). You can swizzle onConversionFailure/onStorageFailure methods using @_dynamicReplacement(for:) to provide your own, custom logic around error handling.