authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Aleksandr Gaidukov's profile image

Aleksandr Gaidukov

Alexander has more than nine years of experience developing applications and over five years with the iOS platform—both iPhone and iPad.

Expertise

Previously At

Accenture
Share

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.

Property Wrappers in Swift 5.1

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 {
    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
}

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 {
    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 @. The property type must match the `wrappedValue` type of wrapper. Now, you can work with this property as with the ordinary one.

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 {
    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 {
    private var range: ClosedRange
    private var value: Value
    init(wrappedValue value: Value, _ range: ClosedRange) {
        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)

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 {
    private let subject = PassthroughSubject()
    var wrappedValue: Value {
	didSet {
	    subject.send(wrappedValue)
	}
    }
    var projectedValue: AnyPublisher {
	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:

  1. 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 our Email example, it is not possible to throw an error if a user tries to set an invalid email. We can return nil or crash the app with a fatalError() call, which could be unacceptable in some cases.
  2. 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.

Consult the author or an expert on this topic.
Schedule a call
Aleksandr Gaidukov's profile image
Aleksandr Gaidukov

Located in 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.

Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

Previously At

Accenture

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Toptal Developers

Join the Toptal® community.