An Introduction to Swift Property Wrappers

Now that the topics of Swift classes and structures have been covered, this chapter will introduce a related topic in the form of property wrappers. Introduced in Swift 5.1, property wrappers provide a way to reduce the amount of duplicated code involved in writing getters, setters and computed properties in class and structure implementations.

1.1 Understanding Property Wrappers

When values are assigned or accessed via a property within a class or structure instance it is sometimes necessary to perform some form of transformation or validation on that value before it is stored or read. As outlined in the chapter entitled The Basics of Object-Oriented Programming in Swift, this type of behavior can be implemented through the creation of computed properties. Frequently, patterns emerge where a computed property is common to multiple classes or structures. Prior to the introduction of Swift 5.1, the only way to share the logic of a computed property was to duplicate the code and embed it into each class or structure implementation. Not only is this inefficient, but a change in the behavior of the computation must be manually propagated across all the entities that use it.

To address this shortcoming, Swift 5.1 introduced a feature known as property wrappers. Property wrappers essentially allow the capabilities of computed properties to be separated from individual classes and structures and reused throughout the app code base.

1.2 A Simple Property Wrapper Example

Perhaps the best way to understand property wrappers is to study a very simple example. Imagine a structure with a String property intended to contain a city name. Such a structure might read as follows:

struct Address {
    var city: String
}

If the class was required to store the city name in uppercase, regardless of how it was entered by the user, a computed property such as the following might be added to the structure:

struct Address {

    private var cityname: String = ""

    var city: String {
        get { cityname }
        set { cityname = newValue.uppercased() }
    }
}

When a city name is assigned to the property, the setter within the computed property converts it to uppercase before storing it in the private cityname variable. This structure can be tested using the following code:

var address = Address()

address.city = "London"
print(address.city)

When executed, the output from the above code would read as follows:

LONDON

Clearly the computed property performs the task of converting the city name string to uppercase, but if the same behavior is needed in other structures or classes the code would need to be duplicated in those declarations. In this example this is only a small amount of code, but that won’t necessarily be the case for more complex computations.

Instead of using a computed property, this logic can instead be implemented as a property wrapper. The following declaration, for example, implements a property wrapper named FixCase designed to convert a string to uppercase:

@propertyWrapper
struct FixCase {
    private(set) var value: String = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.uppercased() }
    }

    init(wrappedValue initialValue: String) {
        self.wrappedValue = initialValue
    }
}

Property wrappers are declared using the @propertyWrapper directive and are implemented in a class or structure (with structures being the preferred choice). All property wrappers must contain a wrappedValue property containing the getter and setter code that changes or validates the value. An optional initializer may also be included which is passed the value being assigned. In this case, the initial value is simply assigned to the wrappedValue property where it is converted to uppercase and stored in the private variable.

Now that this property wrapper has been defined, it can be reused by applying it to other property variables wherever the same behavior is needed. To use this property wrapper, simply prefix property declarations with the @FixCase directive in any class or structure declarations where the behavior is needed, for example:

struct Contact {
    @FixCase var name: String
    @FixCase var city: String
    @FixCase var country: String
}

var contact = Contact(name: "John Smith", city: "London", country: "United Kingdom")

print("\(contact.name), \(contact.city), \(contact.country)")

When executed, the following output will appear:

JOHN SMITH, LONDON, UNITED KINGDOM

1.3 Supporting Multiple Variables and Types

In the above example, the property wrapper accepted a single value in the form of the value to be assigned to the property being wrapped. More complex property wrappers may also be implemented that accept other values that can be used when performing the computation. These additional values are placed within parentheses after the property wrapper name. A property wrapper designed to restrict a value within a specified range might read as follows:

struct Demo {
    @MinMaxVal(min: 10, max: 150) var value: Int = 100
}

The code to implement the above MinMaxVal property wrapper could be written as follows:

@propertyWrapper
struct MinMaxVal {

  var value: Int
  let max: Int
  let min: Int

    init(wrappedValue: Int, min: Int, max: Int) {
         value = wrappedValue
         self.min = min
         self.max = max
  }

  var wrappedValue: Int {

    get { return value }
    
    set {
       if newValue > max {
        value = max
       } else if newValue < min {
        value = min
       } else {
        value = newValue
      }
    }
  }
}

Note that the init() method has been implemented to accept the min and max values in addition to the wrapped value. The wrappedValue setter checks the value and modifies it to the min or max number if it falls above or below the specified range.

The above property wrapper can be tested using the following code:

struct Demo {
    @MinMaxVal(min: 100, max: 200) var value: Int = 100
}

var demo = Demo()
demo.value = 150
print(demo.value)
demo.value = 250
print(demo.value)

When executed, the first print statement will output 150 because it falls within the acceptable range, while the second print statement will show that the wrapper restricted the value to the maximum permitted value (in this case 200).

As currently implemented, the property wrapper will only work with integer (Int) values. The wrapper would be more useful if it could be used with any variable type which can be compared with another value of the same type. Fortunately, protocol wrappers can be implemented to work with any types that conform to a specific protocol. Since the purpose of this wrapper is to perform comparisons, it makes sense to modify it to support any data types that conform to the Comparable protocol which is included with the Foundation framework. Types that conform to the Comparable protocol are able to be used in equality, greater-than and less-than comparisons. A wide range of types such as String, Int, Date, Date Interval and Character conform to this protocol.

To implement the wrapper so that it can be used with any types that conform to the Comparable protocol, the declaration needs to be modified as follows:

@propertyWrapper
struct MinMaxVal<V: Comparable> {
  var value: V
  let max: V
  let min: V

    init(wrappedValue: V, min: V, max: V) {
      value = wrappedValue
      self.min = min
      self.max = max
    }

  var wrappedValue: V {

    get { return value }

    set {
       if newValue > max {
        value = max
       } else if newValue < min {
        value = min
       } else {
        value = newValue
      }
    }
  }
}

The modified wrapper will still work with Int values as before but can now also be used with any of the other types that conform to the Comparable protocol. In the following example, a string value is evaluated to ensure that it fits alphabetically within the min and max string values:

struct Demo {
    @MinMaxVal(min: "Apple", max: "Orange") var value: String = ""
}

var demo = Demo()
demo.value = "Banana"
print(demo.value)
// Banana <--- Value fits within alphabetical range and is stored.

demo.value = "Pear"
print(demo.value)
// Orange <--- Value is outside of the alphabetical range so is changed to the max value.

Similarly, this same wrapper will also work with Date instances, as in the following example where the value is limited to a date between the current date and one month in the future:

struct DateDemo {
     @MinMaxVal(min: Date(), max: Calendar.current.date(byAdding: .month, 
               value: 1, to: Date())! ) var value: Date = Date()
}

The following code and output demonstrate the wrapper in action using Date values:

var dateDemo = DateDemo()

print(dateDemo.value)
// 2019-08-23 20:05:13 +0000. <--- Property set to today by default.

dateDemo.value = Calendar.current.date(byAdding: .day, value: 10, to: Date())! // <--- Property is set to 10 days into the future.

print(dateDemo.value)
// 2019-09-02 20:05:13 +0000 <--- Property is within acceptable range and is stored.

dateDemo.value = Calendar.current.date(byAdding: .month, value: 2, to: Date())! // <--- Property is set to 2 months into the future.

print(dateDemo.value)
// 2019-09-23 20:08:54 +0000 <--- Property is outside range and set to max date (i.e. 1 month into the future).

1.4 Summary

Introduced with Swift 5.1, property wrappers allow the behavior that would normally be placed in the getters and setters of a property implementation to be extracted and reused through the codebase of an app project avoiding the duplication of code within the class and structure declarations. Property wrappers are declared in the form of structures using the @propertyWrapper directive.

Property wrappers are a powerful Swift feature and allow you to add your own custom behavior to the Swift language. In addition to creating your own property wrappers, you will also encounter them when working with the iOS SDK. In fact, pre-defined property wrappers are used extensively when working with SwiftUI as will be covered in later chapters.