How to Approach Wrappers for Swift Properties
A property wrapper is a generic structure that encapsulates read and write access to the property and adds additional behavior to it. We use it if we need to constrain the available property values, add extra logic to the read/write access (like using databases or user defaults), or add some additional methods.
In this article, Toptal Freelance iOS Developer Alexander Gaidukov demonstrates Swift 5.1’s new @propertyWrapper annotation—a more elegant solution.
A property wrapper is a generic structure that encapsulates read and write access to the property and adds additional behavior to it. We use it if we need to constrain the available property values, add extra logic to the read/write access (like using databases or user defaults), or add some additional methods.
In this article, Toptal Freelance iOS Developer Alexander Gaidukov demonstrates Swift 5.1’s new @propertyWrapper annotation—a more elegant solution.
Alexander has more than nine years of experience developing applications and over five years with the iOS platform—both iPhone and iPad.
Previously At
In simple terms, a property wrapper is a generic structure that encapsulates read and write access to the property and adds additional behavior to it. We use it if we need to constrain the available property values, add extra logic to the read/write access (like using databases or user defaults), or add some additional methods.
This article is about a new Swift 5.1 approach to wrapping properties, which introduces a new, cleaner syntax.
Old Approach
Imagine you are developing an application, and you have an object that contains user profile data.
struct Account {
var firstName: String
var lastName: String
var email: String?
}
let account = Account(firstName: "Test",
lastName: "Test",
email: "test@test.com")
account.email = "new@test.com"
print(account.email)
You want to add email verification—if the user email address is not valid, the email
property must be nil
. This would be a good case to use a property wrapper to encapsulate this logic.
struct Email<Value: StringProtocol> {
private var _value: Value?
init(initialValue value: Value?) {
_value = value
}
var value: Value? {
get {
return validate(email: _value) ? _value : nil
}
set {
_value = newValue
}
}
private func validate(email: Value?) -> Bool {
guard let email = email else { return false }
let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64}"
let pred = NSPredicate(format: "SELF MATCHES %@", regex)
return pred.evaluate(with: email)
}
}
We can use this wrapper in the Account structure:
struct Account {
var firstName: String
var lastName: String
var email: Email<String>
}
Now, we are sure that the email property can only contain a valid email address.
Everything looks good, except the syntax.
let account = Account(firstName: "Test",
lastName: "Test",
email: Email(initialValue: "test@test.com"))
account.email.value = "new@test.com"
print(account.email.value)
With a property wrapper, the syntax for initializing, reading, and writing such properties becomes more complex. So, is it possible to avoid this complication and use property wrappers without syntax changes? With Swift 5.1, the answer is yes.
The New Way: @propertyWrapper Annotation
Swift 5.1 provides a more elegant solution to creating property wrappers, where marking a property wrapper with a @propertyWrapper
annotation is allowed. Such wrappers have more compact syntax compared to the traditional ones, resulting in more compact and understandable code. The @propertyWrapper
annotation has only one requirement: Your wrapper object must contain a non-static property called a wrappedValue
.
@propertyWrapper
struct Email<Value: StringProtocol> {
var value: Value?
var wrappedValue: Value? {
get {
return validate(email: value) ? value : nil
}
set {
value = newValue
}
}
private func validate(email: Value?) -> Bool {
guard let email = email else { return false }
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
return emailPred.evaluate(with: email)
}
}
To define such wrapped property in the code, we need to use the new syntax.
@Email
var email: String?
So, we marked the property with the annotation @
email = "valid@test.com"
print(email) // test@test.com
email = "invalid"
print(email) // nil
Great, it looks better now than with the old approach. But our wrapper implementation has one disadvantage: It doesn’t allow an initial value for the wrapped value.
@Email
var email: String? = "valid@test.com" //compilation error.
To resolve this, we need to add the following initializer to the wrapper:
init(wrappedValue value: Value?) {
self.value = value
}
And that’s it.
@Email
var email: String? = "valid@test.com"
print(email) // test@test.com
@Email
var email: String? = "invalid"
print(email) // nil
The final code of the wrapper is below:
@propertyWrapper
struct Email<Value: StringProtocol> {
var value: Value?
init(wrappedValue value: Value?) {
self.value = value
}
var wrappedValue: Value? {
get {
return validate(email: value) ? value : nil
}
set {
value = newValue
}
}
private func validate(email: Value?) -> Bool {
guard let email = email else { return false }
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
return emailPred.evaluate(with: email)
}
}
Configurable Wrappers
Let’s take another example. You are writing a game, and you have a property in which the user scores are stored. The requirement is that this value should be greater than or equal to 0 and less than or equal to 100. You can achieve this by using a property wrapper.
@propertyWrapper
struct Scores {
private let minValue = 0
private let maxValue = 100
private var value: Int
init(wrappedValue value: Int) {
self.value = value
}
var wrappedValue: Int {
get {
return max(min(value, maxValue), minValue)
}
set {
value = newValue
}
}
}
@Scores
var scores: Int = 0
This code works but it doesn’t seem generic. You can’t reuse it with different constraints (not 0 and 100). Moreover, it can constrain only integer values. It would be better to have one configurable wrapper that can constrain any type that conforms to the Comparable protocol. To make our wrapper configurable, we need to add all configuration parameters through an initializer. If the initializer contains a wrappedValue
attribute (the initial value of our property), it must be the first parameter.
@propertyWrapper
struct Constrained<Value: Comparable> {
private var range: ClosedRange<Value>
private var value: Value
init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
self.value = value
self.range = range
}
var wrappedValue: Value {
get {
return max(min(value, range.upperBound), range.lowerBound)
}
set {
value = newValue
}
}
}
To initialize a wrapped property, we define all the configuration attributes in parentheses after the annotation.
@Constrained(0...100)
var scores: Int = 0
The number of configuration attributes is unlimited. You need to define them in parentheses in the same order as in the initializer.
Gaining Access to the Wrapper Itself
If you need access to the wrapper itself (not the wrapped value), you need to add an underscore before the property name. For instance, let’s take our Account structure.
struct Account {
var firstName: String
var lastName: String
@Email
var email: String?
}
let account = Account(firstName: "Test",
lastName: "Test",
email: "test@test.com")
account.email // Wrapped value (String)
account._email // Wrapper(Email<String>)
We need access to the wrapper itself in order to use the additional functionality that we added to it. For instance, we want the Account structure to conform to the Equatable protocol. Two accounts are equal if their email addresses are equal, and the email addresses must be case insensitive.
extension Account: Equatable {
static func ==(lhs: Account, rhs: Account) -> Bool {
return lhs.email?.lowercased() == rhs.email?.lowercased()
}
}
It works, but it is not the best solution because we must remember to add a lowercased() method wherever we compare emails. A better way would be to make the Email structure equatable:
extension Email: Equatable {
static func ==(lhs: Email, rhs: Email) -> Bool {
return lhs.wrappedValue?.lowercased() == rhs.wrappedValue?.lowercased()
}
}
and compare wrappers instead of wrapped values:
extension Account: Equatable {
static func ==(lhs: Account, rhs: Account) -> Bool {
return lhs._email == rhs._email
}
}
Projected Value
The @propertyWrapper
annotation provides one more syntax sugar - a projected value. This property can have any type you want. To access this property, you need to add a $
prefix to the property name. To explain how it works, we use an example from the Combine framework.
The @Published
property wrapper creates a publisher for the property and returns it as a projected value.
@Published
var message: String
print(message) // Print the wrapped value
$message.sink { print($0) } // Subscribe to the publisher
As you can see, we use a message to access the wrapped property, and a $message to access the publisher. What should you do to add a projected value to your wrapper? Nothing special, just declare it.
@propertyWrapper
struct Published<Value> {
private let subject = PassthroughSubject<Value, Never>()
var wrappedValue: Value {
didSet {
subject.send(wrappedValue)
}
}
var projectedValue: AnyPublisher<Value, Never> {
subject.eraseToAnyPublisher()
}
}
As noted earlier, the projectedValue
property can have any type based on your needs.
Limitations
The new property wrappers’ syntax looks good but it also contains several limitations, the main ones being:
- They can’t participate in error handling. The wrapped value is a property (not a method), and we can’t mark the getter or setter as
throws
. For instance, in ourEmail
example, it is not possible to throw an error if a user tries to set an invalid email. We can returnnil
or crash the app with afatalError()
call, which could be unacceptable in some cases. - Applying multiple wrappers to the property is not allowed. For example, it would be better to have a separate
@CaseInsensitive
wrapper and combine it with an@Email
wrapper instead of making the@Email
wrapper case insensitive. But constructions like these are forbidden and lead to compilation errors.
@CaseInsensitive
@Email
var email: String?
As a workaround for this particular case, we can inherit the Email
wrapper from the CaseInsensitive
wrapper. However, the inheritance has limitations too—only classes support inheritance, and only one base class is allowed.
Conclusion
@propertyWrapper
annotations simplify the property wrappers’ syntax, and we can operate with the wrapped properties in the same way as with the ordinary ones. This makes your code, as a Swift Developer more compact and understandable. At the same time, it has several limitations that we have to take into account. I hope that some of them will be rectified in future Swift versions.
If you’d like to learn more about Swift properties, check out the official docs.
Understanding the basics
What is a property wrapper in Swift?
A property wrapper is a generic structure that encapsulates read and write access to the property and adds additional behavior to it.
Why do we need property wrappers?
We use property wrappers if we need to constrain the available property values, change read/write access (like using DB or other storage), or add some additional methods like value validation.
Which version of Swift contains the @propertyWrapper annotation?
The @propertyWrapper annotation is available in Swift 5.1 or later.
What limitations does the wrapper have?
They can’t participate in error handling, and applying multiple wrappers to the property is not allowed.
Aleksandr Gaidukov
Phuket, Thailand
Member since August 31, 2016
About the author
Alexander has more than nine years of experience developing applications and over five years with the iOS platform—both iPhone and iPad.
PREVIOUSLY AT