What is a property wrapper?
We can think of property wrappers as a way to decorate variables and function parameters. We can use it to run additional code every time a variable is accessed or set. One of the most common uses is to reuse repetitive code.
We can create a property wrapper using a Struct
type and decorating the type with @propertyWrapper
.
We have a property wrappedValue
, which we can decorate the getter and setter to execute additional code.
Also we have a projectedValue
property, which we can use to provide additional context. One of the most common use it’s to return a publisher that will listen to changes to the type we are decorating.
var projectedValue: AnyPublisher<Value, Never> {
publisher.eraseToAnyPublisher()
}
Or to return an instance to the property wrapper himself so we can inspect the value or some of the properties of the wrapper instance.
var projectedValue: PrintAccess<Value> {
return self
}
Let’s use it for a simple case, a property wrapper that will print to the console every time the value is accessed or set.
@propertyWrapper
struct PrintAccess<Value> {
private(set) var value: Value
private let publisher = PassthroughSubject<Value, Never>()
init(wrappedValue: Value) {
self.value = wrappedValue
}
var wrappedValue: Value {
get {
print("Getter: value:\(value) \(Date())")
return value
}
set {
self.value = newValue
print("Setter: newValue:\(newValue) \(Date())")
publisher.send(newValue)
}
}
var projectedValue: AnyPublisher<Value, Never> {
publisher.eraseToAnyPublisher()
}
}
//Used on a property
struct ContentViewState {
@PrintAccess(wrappedValue: false) static var isLoaded: Bool
}
//Used on a method
func someMethod(@PrintAccess() someParam: String) {
}
_ = ContentViewState.isLoaded
ContentViewState.isLoaded = true
_ = ContentViewState.isLoaded //Wrapped value
ContentViewState.$isLoaded //Projected value
Output console:
Getter: value:false 2023-01-14 21:45:09 +0000
Setter: newValue:true 2023-01-14 21:45:09 +0000
Getter: value:true 2023-01-14 21:45:09 +0000
As we can see, we access the wrapped value directly using the property and with $
we access the projectedValue
Useful cases
Let’s show some cases where property wrappers can add value to our code.
Store a Codable
object on disk. As we can see, errors are not handled. We could define a projectedValue
publisher which will listen to errors but for a simple case, looks like this:
@propertyWrapper
struct FileStore<Value: Codable> {
private let storeURL: URL
init(name: String) {
let documentsPath = NSSearchPathForDirectoriesInDomains(
.documentDirectory,
.userDomainMask, true)[0]
let url = URL(fileURLWithPath: documentsPath)
self.storeURL = url.appendingPathComponent(
name,
conformingTo: .json)
}
init(url: URL) {
self.storeURL = url
}
var wrappedValue: Value? {
get {
guard let data = try? Data(contentsOf: storeURL) else {
return nil
}
return try? JSONDecoder().decode(Value.self, from: data)
}
set {
if let data = try? JSONEncoder().encode(newValue) {
try? data.write(to: storeURL)
}
}
}
}
struct StoredProperties {
@FileStore<Date>(name: "lastAccess") static var lastAccess: Date?
}
StoredProperties.lastAccess = Date(timeIntervalSince1970: 2)
print(StoredProperties.lastAccess!) // 1970-01-01 00:00:02 +0000
A thread safe wrapper:
@propertyWrapper
struct ThreadSafe<Value> {
var value: Value
let lock = NSLock()
init(wrappedValue value: Value) {
self.value = value
}
var wrappedValue: Value {
get {
lock.lock()
defer { lock.unlock() }
return value
}
set {
lock.lock()
defer { lock.unlock() }
value = newValue
}
}
}