I'm in the process of migrating my data persistence library Boutique from being an ObservableObject
supporting @Observable types. For the most part the process has been straightforward, but I have two property wrappers (@StoredValue
and @SecurelyStoredValue
that now work, with one outstanding question.
These two property wrappers are responsible for updating the value of an item in UserDefaults and the system keychain, respectively. To accomplish this and inform any @Observable objects that hold these values that the value has changed, I'm leaning on ObservationRegistrar.
Here is a code sample of how these property wrappers are expected to be used.
@Observable
final class AppState {
@ObservationIgnored
@StoredValue(key: "funkyRedPandaModeEnabled")
var funkyRedPandaModeEnabled = false
@ObservationIgnored
@SecurelyStoredValue(key: "account")
var userAccount: Account? = nil
}
Elsewhere in the app, you may have a mutation occur (imagine a SwiftUI button) that calls:
self.appState.funkyRedPandaModeEnabled.toggle()
When this occurs, our StoredValue<Bool>
would toggle from true to false (or vice versa). In my demo app I use funkyRedPandaModeEnabled
to determine whether a user sees a rainbow overlay on top of their red panda image, demonstrating how to build a state-driven app using Boutique.
Now for the main question at hand. Both @StoredValue
and @SecurelyStoredValue
use ObservationRegistrar
to inform owners of this property when a value has changed. I use it in any mutating function, such as SecurelyStoredValue's insert()
.
func insert(_ value: Item) throws {
observationRegistrar.willSet(self, keyPath: \.wrappedValue)
let keychainQuery = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: self.keychainService,
kSecAttrAccount: self.key,
kSecValueData: try JSONCoders.encoder.encodeBoxedData(item: value)
]
.withGroup(self.group)
.mapToStringDictionary()
let status = SecItemAdd(keychainQuery as CFDictionary, nil)
if status == errSecSuccess || status == errSecDuplicateItem {
self.valueSubject.send(value)
observationRegistrar.didSet(self, keyPath: \.wrappedValue)
} else {
throw KeychainError(status: status)
}
}
In the example above, I'm only calling observationRegistrar.didSet(self, keyPath: \.wrappedValue)
when the value is successfully changed, because if there is a keychain error, then the value does not actually change so I'm unsure whether to call didSet
.
I couldn't find any documentation for this, but is it expected that if observationRegistrar.willSet
is called that observationRegistrar.didSet
must be called as well?
- If it's only expected to call
didSet
when a successful mutation occurred, then I need to use this strategy of selectively callingdidSet
. - But so I should be able to call
didSet
in a defer block, or usewithMutation
.