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.
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 Swift Object-Oriented Programming, 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.
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
}
Code language: Swift (swift)
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:
You are reading a sample chapter from an old edition of iOS App Development Essentials. Purchase the fully updated iOS 18 App Development Essentials book. The full book contains 71 chapters, over 612 pages of in-depth information, downloadable source code, and access to over 50 SwiftUI knowledge test quizzes. |
struct Address {
private var cityname: String = ""
var city: String {
get { cityname }
set { cityname = newValue.uppercased() }
}
}
Code language: Swift (swift)
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)
Code language: Swift (swift)
When executed, the output from the above code would read as follows:
LONDON
Code language: plaintext (plaintext)
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
}
}
Code language: Swift (swift)
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 include 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.
You are reading a sample chapter from an old edition of iOS App Development Essentials. Purchase the fully updated iOS 18 App Development Essentials book. The full book contains 71 chapters, over 612 pages of in-depth information, downloadable source code, and access to over 50 SwiftUI knowledge test quizzes. |
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)")
Code language: Swift (swift)
When executed, the following output will appear:
JOHN SMITH, LONDON, UNITED KINGDOM
Code language: plaintext (plaintext)
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
}
Code language: Swift (swift)
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
}
}
}
}
Code language: Swift (swift)
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.
You are reading a sample chapter from an old edition of iOS App Development Essentials. Purchase the fully updated iOS 18 App Development Essentials book. The full book contains 71 chapters, over 612 pages of in-depth information, downloadable source code, and access to over 50 SwiftUI knowledge test quizzes. |
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)
Code language: Swift (swift)
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
}
}
}
}
Code language: Swift (swift)
The modified wrapper will still work with Int values as before but can now also be used with any of the other An Introduction to Swift Property Wrappers 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:
You are reading a sample chapter from an old edition of iOS App Development Essentials. Purchase the fully updated iOS 18 App Development Essentials book. The full book contains 71 chapters, over 612 pages of in-depth information, downloadable source code, and access to over 50 SwiftUI knowledge test quizzes. |
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.
Code language: Swift (swift)
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()
}
Code language: Swift (swift)
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).
Code language: Swift (swift)
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.