Cleaner Code With Property Wrappers

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.

Eric Collom Senior Technical Consultant