A property wrapper extracts property logic into a reusable type, the way @AppStorage wraps
user defaults. This recipe builds a @SecureStorage wrapper for the Keychain that also updates
SwiftUI views when its value changes.
The Wrapper Type
Annotate a type with @propertyWrapper and give it a wrappedValue property. Swift routes
every read and write of the wrapped property through it. Swift calls
init(wrappedValue:key:) automatically when the property declares a default value.
@propertyWrapper struct SecureStorage<Value: Codable> {
private let key: String
private let initialValue: Value
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
private let keychain = Keychain(service: Constants.sharedGroup)
init(wrappedValue initialValue: Value, key: String) {
self.key = key
self.initialValue = initialValue
}
var wrappedValue: Value {
get {
guard
let data = try? keychain.getData(key),
let value = try? decoder.decode(Value.self, from: data)
else { return initialValue }
return value
}
set {
guard let data = try? encoder.encode(newValue) else { return }
try? keychain.set(data, key: key)
}
}
}
A wrapper is also a seam for third-party code: the codebase sees only @SecureStorage, so
swapping the KeychainAccess package later touches one file.
Projected Value
Implement projectedValue to make the $ syntax work, the way SwiftUI wrappers hand out
bindings. The projected value can be any type, not only Binding.
var projectedValue: Binding<Value> {
.init(
get: { wrappedValue },
set: { wrappedValue = $0 }
)
}
Driving SwiftUI Updates
A plain wrapper does not refresh views. Two additions fix that: conform the wrapper to
DynamicProperty, and hold the state in an inner ObservableObject observed with
@ObservedObject so a write publishes a change. Note the nonmutating set, which lets a
struct-held wrapper write through the class.
private final class KeychainStorage<Value: Codable>: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
var value: Value {
get { fetch() }
set {
objectWillChange.send()
save(newValue)
}
}
// save(_:) encodes to the keychain, fetch() decodes or returns the default
}
@propertyWrapper struct SecureStorage<Value: Codable>: DynamicProperty {
@ObservedObject private var storage: KeychainStorage<Value>
var wrappedValue: Value {
get { storage.value }
nonmutating set { storage.value = newValue }
}
init(wrappedValue: Value, _ key: String) {
storage = KeychainStorage(defaultValue: wrappedValue, for: key)
}
var projectedValue: Binding<Value> {
.init(get: { wrappedValue }, set: { wrappedValue = $0 })
}
}
Usage reads like the built-in wrappers: @SecureStorage(Settings.goalKey) var goal = 150, with
$goal feeding a Stepper and the view refreshing on every write.