Created
October 10, 2024 17:06
-
-
Save IanKeen/1a0e6e6b47ea407174f005c3fb6ec26f to your computer and use it in GitHub Desktop.
PropertyWrapper: ObservableAppStorage - Make UserDefaults work with @observable models (without all the boilerplate)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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