In the previous episode, you learned about the anatomy of property wrappers. Property wrappers aim to reduce code duplication and improve the readability of the code you write. They also implicitly document the code when implemented and applied correctly. That is an added bonus. In this episode, we explore a few additional features of property wrappers.
No Magic Involved
Remember from the previous episode that the compiler synthesizes code every time it encounters a wrapped property. The code the compiler synthesizes provides storage for the property wrapper and access to the wrapped property through the property wrapper. Notice that the property wrapper is only accessible by the Book struct. The title property is a computed property that provides access to the wrapped property through the property wrapper. This example clarifies why a wrapped property can be accessed the same way a regular property can be accessed. It is important that you understand this pattern. As you can see, there is no magic involved.
struct Book {
// MARK: - Properties
// @Capitalized var title: String = "title"
private var _title = Capitalized(wrappedValue: "title")
var title: String {
get {
_title.wrappedValue
}
set {
_title.wrappedValue = newValue
}
}
}
Assigning an Initial Value
It is still possible to assign an initial value to a wrapped property. To enable this capability, the property wrapper needs to define an initializer that accepts an argument with name wrappedValue. The Capitalized property wrapper already implements such an initializer, which means we can assign an initial value to the title property of the Book struct. The initial value is automatically passed to the init(wrappedValue:) initializer of the property wrapper.
struct Book {
// MARK: - Properties
@Capitalized var title: String = "title"
}
var book = Book()
book.title // "Title"
Configuring a Property Wrapper
The Capitalized property wrapper only scratches the surface. Let's take a look at a more sophisticated example. The property wrapper we implement next wraps a Date object and has the ability to format the Date object it wraps.
We define a struct with name FormattedDate. We prefix the type definition with the propertyWrapper attribute and define a variable property, wrappedValue, of type Date. The property wrapper type and the wrappedValue property are required to have the same access level.
import Foundation
@propertyWrapper
struct FormattedDate {
// MARK: - Properties
var wrappedValue: Date
}
The FormattedDate struct defines two private, constant properties, dateFormat of type String and dateFormatter of type DateFormatter.
import Foundation
@propertyWrapper
struct FormattedDate {
// MARK: - Properties
private let dateFormat: String
// MARK: -
private let dateFormatter = DateFormatter()
// MARK: -
var wrappedValue: Date
}
Because dateFormat needs to have an initial value, we implement an initializer that accepts the wrapped value as an argument and assigns a default value to the dateFormat property.
import Foundation
@propertyWrapper
struct FormattedDate {
// MARK: - Properties
private let dateFormat: String
// MARK: -
private let dateFormatter = DateFormatter()
// MARK: -
var wrappedValue: Date
// MARK: - Initialization
init(wrappedValue: Date) {
self.wrappedValue = wrappedValue
self.dateFormat = "MM/dd/yyyy"
}
}
At the moment, the FormattedDate property wrapper does nothing more than wrapping a Date object. Let's put the property wrapper to use. We complete its implementation in a few minutes. We define a struct with name Document. The struct defines two properties, createdAt and updatedAt. Both properties are of type Date.
struct Document {
// MARK: - Properties
var createdAt: Date
var updatedAt: Date
}
We assign the current date and time to createdAt and updatedAt, and apply the FormattedDate property wrapper to both properties.
struct Document {
// MARK: - Properties
@FormattedDate var createdAt: Date = Date()
@FormattedDate var updatedAt: Date = Date()
}
We can pass additional options to the property wrapper by defining custom initializers. Let's implement an initializer that accepts the wrapped value and the date format.
import Foundation
@propertyWrapper
struct FormattedDate {
// MARK: - Properties
private let dateFormat: String
// MARK: -
private let dateFormatter = DateFormatter()
// MARK: -
var wrappedValue: Date
// MARK: - Initialization
init(wrappedValue: Date) {
self.wrappedValue = wrappedValue
self.dateFormat = "MM/dd/yyyy"
}
init(wrappedValue: Date, dateFormat: String) {
self.wrappedValue = wrappedValue
self.dateFormat = dateFormat
}
}
By implementing the init(wrappedValue:dateFormat:) initializer, we can configure the property wrapper by passing it a date format. Let's define a custom date format for the updatedAt property. This is what that looks like.
struct Document {
// MARK: - Properties
@FormattedDate var createdAt: Date = Date()
@FormattedDate(dateFormat: "MMM d, yyyy") var updatedAt: Date = Date()
}
This is convenient, but you may have noticed that the property wrapper doesn't use the dateFormat property. Let's fix that. Property wrappers can expose additional functionality by defining a projected value. We define a computed property with name, projectedValue, of type String?. In the getter of the computed property, we set the dateFormat property of the date formatter and convert the date stored in wrappedValue to a string by invoking the date formatter's string(from:) method.
import Foundation
@propertyWrapper
struct FormattedDate {
// MARK: - Properties
private let dateFormat: String
// MARK: -
private let dateFormatter = DateFormatter()
// MARK: -
var wrappedValue: Date
// MARK: -
var projectedValue: String? {
// Configure Date Formatter
dateFormatter.dateFormat = dateFormat
// Convert Date to String
return dateFormatter.string(from: wrappedValue)
}
// MARK: - Initialization
init(wrappedValue: Date) {
self.wrappedValue = wrappedValue
self.dateFormat = "MM/dd/yyyy"
}
init(wrappedValue: Date, dateFormat: String) {
self.wrappedValue = wrappedValue
self.dateFormat = dateFormat
}
}
We can access the projected value through the property by prefixing the name of the property with a dollar sign, $. The property wrapper allows access to the wrapped property, a Date object, as well as the projected value, the formatted date.
Give it a try. Create a Document object and access the wrapped properties and the projected values.
let document = Document()
document.createdAt // Date Object
document.updatedAt // Date Object
document.$createdAt // 07/28/2020
document.$updatedAt // Jul 28, 2020
Projected values are powerful and versatile. Combine's Published property wrapper exposes a publisher of the property's type through the projected value. The SwiftUI framework also relies heavily on property wrappers and projected values.
Composition
Another nice feature of property wrappers is that they are composable. This means that a property can have multiple property wrappers. Property wrapper composition sounds nice on paper, but it is quite tricky in practice. Applying multiple property wrappers to a property is possible by nesting later wrapper types inside earlier wrapper types. This implies that the order of the property wrappers is important, that is, property wrapper composition is not commutative. Composition of property wrappers is only possible by nesting property wrappers and that is where it becomes complex and easily leads to compiler errors.
Limitations and Considerations
There are a few restrictions you need to be aware of when using property wrappers. A wrapped property cannot be overridden in a subclass and it isn't possible to define custom accessors for a wrapped property. It isn't possible to declare a wrapped property in an extension or a protocol. A wrapped property cannot be lazy, weak, or unowned, and it shouldn't have the NSCopying or NSManaged attributes applied to it.
Property wrappers can result in clean, readable, expressive code, but, when used incorrectly, they can lead to complex, difficult to understand code. It is important to keep your code transparent and readable. Don't overload property wrappers with logic. Keep them lightweight and focused. The name of the property wrapper should clearly communicate its intent and a property wrapper shouldn't perform tasks it wasn't designed for.
If a developer not familiar with the project browses the codebase, they should be able to understand what the property wrapper does without having to dig into property wrapper's implementation.
What's Next?
Property wrappers are a wonderful addition to Swift. Combine and SwiftUI make heavy use of property wrappers so it is important that you take the time to become familiar with the concept as well as the ins and outs of property wrappers. Even though you can write Swift without relying on property wrappers, it is clear that property wrappers will quickly become a widespread pattern to create clean, readable, and expressive APIs in Swift.