Property wrappers were introduced in Swift 5.1 and are something that many of us have used when working with the Combine framework and SwiftUI. A couple examples would be @State
and @Published
. While these things might look like some weird syntax that does magical things, it is good to have some basic understanding of what property wrappers do under the hood and how to use them.
In a Nutshell
To understand what property wrapper means let’s first understand what property means in Swift. A property is a variable inside of a class, structure, or enum. The wrapper typically contains either getter, setter, or init logic. So, in essense a property wrapper encapsulates logic and wraps it around a variable. This also means that property wrappers cannot be used on variables outside of the scope of a class, structure, or enum.
Here is a simple example of how to create a property wrapper:
@propertyWrapper
public struct PlusOne {
public var wrappedValue: Int
public init(wrappedValue: Int) {
self.wrappedValue = wrappedValue + 1
}
}
As you can see it it just a struct with @propertyWrapper
at the beginning. This is how you would use this property wrapper:
@PlusOne let num = 1
print(num) // prints out 2
PlusOne adds one to the variable when it gets initialized. The value you would get back when checking num would be 2
.
Since property wrappers are really just structs you can also apply generics. If we wanted the above example to work for more number types we could change the implementation to this:
@propertyWrapper
public struct PlusOne<T: Numeric> {
public var wrappedValue: T
public init(wrappedValue: T) {
self.wrappedValue = wrappedValue + 1
}
}
This allows you to use it on anything that implements the numeric protocol such as Double
, Float
, UInt
.
Side Note:
You can also use and initialize property wrappers outside of the @
syntax:
let plusOne = PlusOne<Int>(wrappedValue: 1)
print(plusOne.wrappedValue) // prints out 2
This is usually not very useful but there might be scenarios such as testing property wrappers that handle Codable
and encode/decode logic. This is because Codable
under the hood uses and expects the property wrapper’s type.
Property Observers
Let’s look at a more useful example that also takes advantage of the property observers get and set:
@propertyWrapper
public struct Atomic<T> {
private let queue = DispatchQueue(label: "ATOMIC_VARIABLE_QUEUE_\(UUID().uuidString)")
private var value: T
public init(wrappedValue: T) {
self.value = wrappedValue
}
public var wrappedValue: T {
get {
return queue.sync { value }
}
set {
queue.sync { value = newValue }
}
}
}
This property wrapper makes things atomic or in other words read/write thread safe. It does this by having a DispatchQueue
which is accessed with sync
in get and set to make read write access synchronous. Here is how it can be applied:
@Atomic var list: [Int]
@Atomic var count: Int
@Atomic var cache: [String: String]
@Atomic var object: MyObject
This highlights the power of property wrappers. Needing atomic data is a common problem and is often solved by manipulating getters and setters. However, it’s nice not to clog up your view models with this logic every time you need an atomic variable. You could implement an Atomic<T>
, non property wrapper, type but it can also obfuscate what the underlying wrapped type is and make your code little harder to read since you have to dive into the Atomic<T>
type and extract the saved value.
For example, going back to our PlusOne implementation. If we implemented it not as a property wrapper:
struct PlusOne<T: Numeric> {
var value: T
init(_ value: T) {
self.value = value + 1
}
}
This would result in the same functionality but harder to read and use code:
var num = PlusOne(1)
To access the underlying value you need to do this:
num.value
This is the biggest thing property wrappers simplify. You don’t need to dig down to access the value and they don’t change the type of the variable. The PlusOne
type is abstracted away! Here is an example showing that the properties type does not change:
@PlusOne var num = 1
print(type(of: num)) // prints out "Int" not PlusOne
print(num is PlusOne<Int>) // prints out false
Using Parameters & Projected Values
Let’s imagine a scenario where we have a text field where the user must put in their current zip code. Before moving on to the next screen we want to make sure that they entered five digits. We can solve this problem with property wrappers and design it so that it would be easy to add additional rules such as en email validation.
@propertyWrapper
struct TextRule {
private var value: String
private var rule: Rule
private(set) var projectedValue: Bool
var wrappedValue: String {
get {
return value
}
set {
projectedValue = true
value = newValue
projectedValue = rule.validate(newValue)
}
}
init(wrappedValue: String = "",_ rule: Rule) {
self.value = wrappedValue
self.rule = rule
projectedValue = rule.validate(wrappedValue)
}
}
enum Rule {
case zipCode
func validate(_ value: String) -> Bool {
switch self {
case .zipCode:
let characterSet = CharacterSet(charactersIn: value)
let isOnlyDigits = CharacterSet.decimalDigits.isSuperset(of: characterSet)
return isOnlyDigits && value.count == 5
}
}
}
In this example we have a lot more going on. We have implemented an init that takes in the wrapped value and a Rule type. The compiler is smart enough to infer the wrappedValue when initializing which in the case below is “12345”:
@TextRule(.zipCode) var zipCode = "12345"
In the init and wrappedValue
setter we run the validaion code and then update the projectedValue
. By just looking at the value of zipCode
we can not tell if it is valid. This is where the projected value comes in. To check if it passed validation you add $
before the variable name:
@TextRule(.zipCode) var zipCode = "12345"
print($zipCode) // prints true
zipCode = "1234z"
print($zipCode) // prints false since it has a non digit character
zipCode = "1234"
print($zipCode) // prints false since it only has 4 digits
Whenever the variables value changes the property wrapper runs validation code and updates the projected value with true
if it is valid or false
if it is not.
Note: projectedValue
is built into property wrappers. Here is swift.orgs description of projected values:
In addition to the wrapped value, a property wrapper can expose additional functionality by defining a projected value — for example, a property wrapper that manages access to a database can expose a flushDatabaseConnection() method on its projected value. The name of the projected value is the same as the wrapped value, except it begins with a dollar sign ($). Because your code can’t define properties that start with $ the projected value never interferes with properties you define.
Downsides
One issue you might come across is that property wrappers do not always play well with optionals. For example if we try to use our PlusOne implementation with an optional Int we get this compile error:
@PlusOne var num: Int? // Generic struct 'PlusOne' requires that 'Int?' conform to 'Numeric'
You might think to yourself, easy! We can just change the propertyWrapper variable to be T?
like so:
@propertyWrapper
public struct PlusOne<T: Numeric> {
public var wrappedValue: T?
public init(wrappedValue: T? = nil) {
guard let wrappedValue = wrappedValue else { return }
self.wrappedValue = wrappedValue + 1
}
}
However you will then run into another issue when applying it to a non-optional:
@PlusOne var num: Int // Generic parameter 'T' could not be inferred
It is often easy to forget that optional is really an enum that either wraps around another type via an associated value. The two enum values in this case would be .some(wrappedValue)
& .none
which is equivalent to nil
. Int?
is really Optional<Int>
not Int
. This adds clarity to the first error, Generic struct 'PlusOne' requires that 'Int?' conform to 'Numeric'
since Optional<Int>
does not implement Numeric
even though wrapped value does. We often don’t think of things this way since non optionals usually just work.
There are solutions to this but they require extending types outside of the property wrapper which isn’t ideal for all scenarios. Here is how one person solved this.
What else?
There are more areas to explore that I have not touched on here. For example, using property wrappers to apply default values, when decoding data, if data is missing, nil, or empty. Using property wrappers to avoid implementing a CodingKeys
enum if one of your variables names does not match up in the data being decoded.