Swift: Property wrappers
Property wrappers enable you to reuse code that specifies the access pattern of a property.
Often you add behaviour to a property through its observers. For example, you can use the didSet
observer to trim a string:
class TestClass {
var trimmedString: String = "" {
didSet {
trimmedString = trimmedString.trimmingCharacters(in: .whitespaces)
}
}
}
var test = TestClass()
test.trimmedString = " test"
print(test.trimmedString) //"test"
This is very useful. However, if you want to add this behaviour to several properties, you have to implement this behaviour separately for each property. Instead, you can define a so called property wrapper.
You create a property wrapper by creating a struct, class or enum that uses the @propertyWrapper
annotation. Inside the property wrapper you have to implement the wrappedValue
property. By implementing property observers of wrappedValue
you can add custom behaviour.
Note that observers of stored properties (didSet
, willSet
) get not called at initialisation time. Hence, you also have to implement the init
method.
Let’s create a property wrapper for the use case of trimming a string:
@propertyWrapper struct WhiteSpacesTrimmed {
var wrappedValue: String {
didSet {
print("propertyWrapper didSet")
wrappedValue = wrappedValue.trimmingCharacters(in: .whitespaces)
}
}
init(wrappedValue:String) {
self.wrappedValue = wrappedValue.trimmingCharacters(in: .whitespaces)
}
}
A property wrapper can then be applied to every property that has the corresponding type:
class TestClass {
@WhiteSpacesTrimmed var test = ""
@WhiteSpacesTrimmed var test2 = ""
}
let testClass = TestClass()
testClass.test = " test"
print(testClass.test) //"test"
As you can see, you just have to add the annotation @WhiteSpacesTrimmed.
wrappedValue as a computed property
In the last example, the wrapped value was a stored property. It’s also possible to use a computed value:
@propertyWrapper struct Max100 {
private var value: Int = 0
var wrappedValue: Int {
get {
return value
}
set {
value = min(100, newValue)
}
}
init(wrappedValue: Int) {
self.wrappedValue = wrappedValue
}
}
class TestClass2 {
@Max100 var test = 0
}
let testClass2 = TestClass2()
testClass2.test = 300
print(testClass2.test)
In this example we created a property wrapper that limits an integer property to the maximum value of 100.
Customizing a property wrapper
As you have seen in the last example, a property wrapper can have properties of its own. But it’s also possible to initialise these properties with custom values every time you use the property wrapper.
In the following example we create a property wrapper that writes and saves a string value to the user defaults. The name of the user defaults key is customizable:
@propertyWrapper
struct UserDefaultsWrapper {
var key: String
var wrappedValue: String? {
set {
UserDefaults.standard.set(newValue, forKey: key)
}
get {
UserDefaults.standard.string(forKey: key)
}
}
}
class TestClass3 {
@UserDefaultsWrapper(key: "testKey") var test: String?
}
If your property wrapper has properties, they can be initialised. If they have an initial value, they’re are optional. If they lack an initial value, they are mandatory.
Projected values
Besides wrappedValue
there is another special property inside a property wrapper: projectedValue
. This value is accessible from the outside by using the symbol $
. Hence, you can use it to provide additional functionality.
In the following example we again create a property wrapper that trims a string. But in this case we save the initial value inside the projectedValue
property:
@propertyWrapper struct WhiteSpacesTrimmed {
var projectedValue: String
var wrappedValue: String {
didSet {
self.projectedValue = wrappedValue
wrappedValue = wrappedValue.trimmingCharacters(in: .whitespaces)
}
}
init(wrappedValue:String) {
self.projectedValue = wrappedValue
self.wrappedValue = wrappedValue.trimmingCharacters(in: .whitespaces)
}
}
class TestClass5 {
@WhiteSpacesTrimmed var test = " test"
}
print("projected Values")
var testClass5 = TestClass5()
print(testClass5.test) //"test"
print(testClass5.$test) //" test"
References
Title image: @ Foxys Forest Manufacture / shutterstock.com