Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Created October 10, 2024 17:06
Show Gist options
  • Save IanKeen/1a0e6e6b47ea407174f005c3fb6ec26f to your computer and use it in GitHub Desktop.
Save IanKeen/1a0e6e6b47ea407174f005c3fb6ec26f to your computer and use it in GitHub Desktop.
PropertyWrapper: ObservableAppStorage - Make UserDefaults work with @observable models (without all the boilerplate)
let prefs = AppPreferences()
print(prefs.foo)
withObservationTracking {
_ = prefs.foo
} onChange: {
print("NEW VALUE!")
DispatchQueue.main.async {
print(prefs.foo)
}
}
prefs.foo = 123
/* Output:
42
NEW VALUE!
123
*/
@Observable
public final class AppPreferences {
@ObservableAppStorage("somePreference")
@ObservationIgnored
public var foo: Int = 42
}
// This is required because the macro generated `access` and `withMutation` members have `internal` access.
// We need this indirection to make them work with the property wrapper because it must have `public` access.
// IF your `_Observable` type doesn't need to be public then you can remove the underscores from the `_Observable`
// protocol and property wrapper, then you can simplify this conformance to:
//
// extension AppPreferences: _Observable { }
//
extension AppPreferences: _Observable {
public func _access<Member>(keyPath: KeyPath<AppPreferences, Member>) {
access(keyPath: keyPath)
}
public func _withMutation<Member, MutationResult>(keyPath: KeyPath<AppPreferences, Member>, _ mutation: () throws -> MutationResult) rethrows -> MutationResult {
try withMutation(keyPath: keyPath, mutation)
}
}
import Observation
public protocol _Observable: Observable {
nonisolated
func _access<Member>(keyPath: KeyPath<Self, Member>)
nonisolated
func _withMutation<Member, MutationResult>(
keyPath: KeyPath<Self, Member>,
_ mutation: () throws -> MutationResult
) rethrows -> MutationResult
}
@propertyWrapper
public struct ObservableAppStorage<Value> {
private let key: String
private let `default`: Value
private let userDefaults: UserDefaults
public var wrappedValue: Value {
get { fatalError() }
set { fatalError() }
}
public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) {
self.key = key
self.default = wrappedValue
self.userDefaults = store
}
public static subscript<Instance: _Observable>(
_enclosingInstance instance: Instance,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<Instance, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<Instance, Self>
) -> Value {
get {
instance._access(keyPath: wrappedKeyPath)
guard let value = instance[keyPath: storageKeyPath].userDefaults.object(forKey: instance[keyPath: storageKeyPath].key) as? Value else {
return instance[keyPath: storageKeyPath].default
}
return value
}
set {
instance._withMutation(keyPath: wrappedKeyPath) {
instance[keyPath: storageKeyPath].userDefaults.set(newValue, forKey: instance[keyPath: storageKeyPath].key)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment