Property Wrappers

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
        }
    }
}