Skip to content

Instantly share code, notes, and snippets.

@eliyap
Created July 6, 2022 23:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eliyap/18a5500d059f0df5c18757045cf8894a to your computer and use it in GitHub Desktop.
Save eliyap/18a5500d059f0df5c18757045cf8894a to your computer and use it in GitHub Desktop.
import Foundation
import Combine
protocol Initializable: RawRepresentable {
init?(rawValue: RawValue)
}
enum Person: Int, Initializable {
case andrew
}
final class Preferences {
static let standard = Preferences(userDefaults: .standard)
fileprivate let userDefaults: UserDefaults
/// Sends through the changed key path whenever a change occurs.
var preferencesChangedSubject = PassthroughSubject<AnyKeyPath, Never>()
init(userDefaults: UserDefaults) {
self.userDefaults = userDefaults
}
@UserDefault("should_show_hello_world")
var shouldShowHelloWorld: Person = .andrew
}
@propertyWrapper
struct UserDefault<Value: Initializable> {
let key: String
let defaultValue: Value
var wrappedValue: Value {
get { fatalError("Wrapped value should not be used.") }
set { fatalError("Wrapped value should not be used.") }
}
init(wrappedValue: Value, _ key: String) {
self.defaultValue = wrappedValue
self.key = key
}
public static subscript(
_enclosingInstance instance: Preferences,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<Preferences, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<Preferences, Self>
) -> Value {
get {
let container = instance.userDefaults
let key = instance[keyPath: storageKeyPath].key
let defaultValue = instance[keyPath: storageKeyPath].defaultValue
let value = container.object(forKey: key)
if Value.self is (any Initializable), let value = value as? Value.RawValue {
return Value(rawValue: value) ?? defaultValue
} else {
return value as? Value ?? defaultValue
}
}
set {
let container = instance.userDefaults
let key = instance[keyPath: storageKeyPath].key
container.set(newValue, forKey: key)
instance.preferencesChangedSubject.send(wrappedKeyPath)
}
}
}
@eliyap
Copy link
Author

eliyap commented Jul 7, 2022

protocol ExtensibleProtocol {
    associatedtype Value
    static func interpret(_ thing: Any?) -> Value?
}

extension ExtensibleProtocol {
    static func interpret(_ thing: Any?) -> Value? {
        thing as? Value
    }
}

extension ExtensibleProtocol where Value: RawRepresentable {
    static func interpret(_ thing: Any?) -> Value? {
        guard let raw = thing as? Value.RawValue else { return nil }
        return Value(rawValue: raw)
    }
}

@propertyWrapper
struct UserDefault<Value>: ExtensibleProtocol {
    let key: String
    let defaultValue: Value

    var wrappedValue: Value {
        get { fatalError("Wrapped value should not be used.") }
        set { fatalError("Wrapped value should not be used.") }
    }
    
    init(wrappedValue: Value, _ key: String) {
        self.defaultValue = wrappedValue
        self.key = key
    }
    
    public static subscript(
        _enclosingInstance instance: Preferences,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<Preferences, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<Preferences, Self>
    ) -> Value {
        get {
            let container = instance.userDefaults
            let key = instance[keyPath: storageKeyPath].key
            let defaultValue = instance[keyPath: storageKeyPath].defaultValue
            let value = container.object(forKey: key)
            
            return interpret(value) ?? defaultValue
        }
        set {
            let container = instance.userDefaults
            let key = instance[keyPath: storageKeyPath].key
            container.set(newValue, forKey: key)
            instance.preferencesChangedSubject.send(wrappedKeyPath)
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment