12.
Properties
Written by Eli Ganim
Chapter 11, “Structures”, showed that you can use structures to group related properties and behaviors into a custom type.
In the example below, the Car structure has two properties; both are constants that store String values:
struct Car {
let make: String
let color: String
}
The values inside a structure are called properties. The two properties of Car are stored properties, which means they store actual string values for each instance of Car.
Some properties calculate values rather than store them. In other words, there’s no actual memory allocated for them; instead, they get calculated on-the-fly each time you access them. Naturally, these are called computed properties.
In this chapter, you’ll learn about both kinds of properties. You’ll also learn some other neat tricks for working with properties, such as how to monitor changes in a property’s value and delay the initialization of a stored property.
Stored Properties
As you may have guessed from the example in the introduction, you’re already familiar with the features of stored properties.
To review, imagine you’re building an address book. You’ll need a Contact type:
struct Contact {
var fullName: String
var emailAddress: String
}
You can use this structure repeatedly, letting you build an array of contacts, each with a different value. The properties you want to store are an individual’s full name and email address.
These are the properties of the Contact structure. You provide a data type for each but opt not to assign a default value because you plan to assign the value upon initialization. After all, the values will differ for each instance of Contact.
Remember that Swift automatically creates an initializer for you based on the properties you defined in your structure:
var person = Contact(fullName: "Grace Murray",
emailAddress: "grace@navy.mil")
You can access the individual properties using dot notation:
person.fullName // Grace Murray
person.emailAddress // grace@navy.mil
You can assign values to properties as long as they’re defined as variables and the parent instance is stored in a variable. That means both the property and the structure containing the property must be declared with var instead of let.
When Grace married, she changed her last name:
person.fullName = "Grace Hopper"
person.fullName // Grace Hopper
Since the property is a variable, she could update her name.
If you’d like to prevent a value from changing, you can define a property as a constant using let, like so:
struct Contact {
var fullName: String
let emailAddress: String
}
// Error: cannot assign to a constant
person.emailAddress = "grace@gmail.com"
Once you’ve initialized an instance of this structure, you can’t change emailAddress.
Default Values
If you can make a reasonable assumption about the value of a property when the type is initialized, you can give that property a default value.
It doesn’t make sense to create a default name or email address for a contact, but imagine you add a new property relationship to indicate what kind of contact it is:
struct Contact {
var fullName: String
let emailAddress: String
var relationship = "Friend"
}
By assigning a value in the definition of relationship, you give this property a default value. Any contact created will automatically be a friend unless you change the value of relationship to something like “Work” or “Family”.
Swift will notice which properties you have defaulted and create the member-wise initializer with parameters also defaulted, so you don’t need to specify them unless you want to.
var person = Contact(fullName: "Grace Murray",
emailAddress: "grace@navy.mil")
person.relationship // Friend
var boss = Contact(fullName: "Ray Wenderlich",
emailAddress: "ray@kodeco.com",
relationship: "Boss")
You can choose to specify the relationship if you want to; otherwise, it takes on the value "Friend".
Computed Properties
Most of the time, properties are stored data, but some can just be computed, which means they perform a calculation before returning a value.
While a stored property can be a constant or a variable, a computed property must be defined as a variable.
Computed properties must also include a type because the compiler needs to know what to expect as a return value.
The measurement for a TV is the perfect use case for a computed property:
The industry definition of the screen size of a TV isn’t the screen’s height or width but its diagonal measurement:
struct TV {
var height: Double
var width: Double
// 1
var diagonal: Int {
// 2
let result = (height * height +
width * width).squareRoot().rounded()
// 3
return Int(result)
}
}
Let’s go through this code one step at a time:
- You use an
Inttype for yourdiagonalproperty. AlthoughheightandwidthareDoubletypes, TV sizes are usually advertised as nice, round numbers such as 50” rather than 49.52”. Instead of the usual assignment operator=to assign a value as you would for a stored property, you use curly braces to enclose your computed property’s calculation. - As you’ve seen in this book, geometry can be handy; once you have the width and height, you can use the Pythagorean theorem to calculate the diagonal length. You use the
roundedmethod to round the value with the standard rule: If the decimal is 0.5 or above, it rounds up; otherwise, it rounds down. - Now that you’ve got a properly-rounded number, you return it as an
Int. Had you convertedresultdirectly toIntwithout rounding first, the result would have been truncated, so 109.99 would have become 109.
Computed properties don’t store any values; they return values based on calculations. From outside of the structure, a computed property can be accessed just like a stored property.
Test this with the TV size calculation:
var tv = TV(height: 53.93, width: 95.87)
tv.diagonal // 110
You have a 110-inch TV. Let’s say you decide you don’t like the standard movie aspect ratio and would instead prefer a square screen. You cut off some of the screen’s width to make it equivalent to the height:
tv.width = tv.height
tv.diagonal // 76
Now you only have a 76-inch square screen. The computed property automatically provides the new value based on the new width.
Mini-Exercise
Do you have a television or a computer monitor? Measure the height and width, plug it into a TV struct, and see if the diagonal measurement matches what you think it is.
Getter and Setter
The computed property you wrote in the previous section is called a read-only computed property. It has a block of code to compute the property’s value, called the getter.
It’s also possible to create a read-write computed property with two code blocks: a getter and a setter.
This setter works differently than you might expect.
As the computed property has no place to store a value, the setter usually sets one or more related stored properties indirectly:
var diagonal: Int {
// 1
get {
// 2
let result = (height * height +
width * width).squareRoot().rounded()
return Int(result)
}
set {
// 3
let ratioWidth = 16.0
let ratioHeight = 9.0
// 4
let ratioDiagonal = (ratioWidth * ratioWidth +
ratioHeight * ratioHeight).squareRoot()
height = Double(newValue) * ratioHeight / ratioDiagonal
width = height * ratioWidth / ratioHeight
}
}
Here’s what’s happening in this code:
- Because you want to include a setter, you now have to be explicit about which calculations comprise the getter and which the setter, so you surround each code block with curly braces and precede it with either
getorset. This specificity isn’t required for read-only computed properties, as their single code block is implicitly a getter. - You use the same code as before to get the computed value.
- For a setter, you usually have to make some kind of assumption. In this case, you provide a reasonable default value for the screen ratio.
- The formulas to calculate height and width, given a diagonal and a ratio, are a bit deep. You could work them out with a bit of time, but I’ve done the dirty work for you and provided them here. The important parts to focus on are:
- The
newValueconstant lets you use whatever value was passed in during the assignment. - Remember, the
newValueis anInt, so to use it in a calculation with aDouble, you must first convert it to aDouble. - Once you’ve done the calculations, you assign the height and width properties of the
TVstructure.
In addition to setting the height and width directly, you can set them indirectly by setting the diagonal computed property. When you set this value, your setter will calculate and store the height and width.
Notice there’s no return statement in a setter — it only modifies the other stored properties. With the setter in place, you have a nice little screen size calculator:
tv.diagonal = 70
tv.height // 34.32...
tv.width // 61.01...
Now you can finally figure out the biggest TV you can cram into your cabinet — you’re so welcome. :]
Type Properties
In the previous section, you learned how to declare stored and computed properties for instances of a particular type. The properties on your instance of TV are separate from the properties on my instance of TV.
However, the type itself may also need properties that are common across all instances. These properties are called type properties.
Imagine you’re building a game with many levels. Each level has a few attributes or stored properties:
struct Level {
let id: Int
var boss: String
var unlocked: Bool
}
let level1 = Level(id: 1, boss: "Chameleon", unlocked: true)
let level2 = Level(id: 2, boss: "Squid", unlocked: false)
let level3 = Level(id: 3, boss: "Chupacabra", unlocked: false)
let level4 = Level(id: 4, boss: "Yeti", unlocked: false)
You can use a type property to store the game’s progress as the player unlocks each level. A type property is declared with the modifier static:
struct Level {
static var highestLevel = 1
let id: Int
var boss: String
var unlocked: Bool
}
Here, highestLevel is a property on Level itself rather than on the instances. That means you don’t access this property on an instance:
// Error: you can’t access a type property on an instance
let highestLevel = level3.highestLevel
Instead, you access it on the type itself:
Level.highestLevel // 1
Using a type property means you can retrieve the same stored property value from anywhere in the code for your app or algorithm. The game’s progress is accessible from any level or any other place in the game, like the main menu.
Property Observers
For your Level implementation, it would be useful to automatically set the highestLevel when the player unlocks a new one. For that, you’ll need a way to listen to property changes. Thankfully, there are a couple of property observers that get called before and after property changes.
The willSet observer is called when a property is about to be changed. The didSet observer is called after a property has been changed. Their syntax is similar to getters and setters:
struct Level {
static var highestLevel = 1
let id: Int
var boss: String
var unlocked: Bool {
didSet {
if unlocked && id > Self.highestLevel {
Self.highestLevel = id
}
}
}
}
When the player unlocks a new level, it will update the highestLevel type property if the level is a new high. There are a couple of things to note here:
- You can access the value of
unlockedfrom inside thedidSetobserver. Remember thatdidSetgets called after the value has been set. - Even though you’re inside an instance of the type, you still have to access type properties with the type name prefix. You must use the full name
Level.highestLevelrather than justhighestLevelto indicate you’re accessing a type property. You can also refer to the static property from within the type asSelf.highestLevel. UsingSelfhere is preferred because even if you change the name of the type to something else — say,GameLevel— the code would still work. The uppercaseSelfindicates you’re accessing a property on the type itself, not an instance property.
willSet and didSet observers are only available for stored properties. If you want to listen for changes to a computed property, add the relevant code to the property’s setter.
Also, remember that the willSet and didSet observers are not called when a property is set during initialization; they only get called when you assign a new value to a fully initialized instance. That means property observers are only useful for variable properties since constant properties are only set during initialization.
Limiting a Variable
You can also use property observers to limit the value of a variable. Say you had a light bulb that could only support a maximum current flowing through its filament.
struct LightBulb {
static let maxCurrent = 40
var current = 0 {
didSet {
if current > LightBulb.maxCurrent {
print("""
Current is too high,
falling back to previous setting.
""")
current = oldValue
}
}
}
}
In this example, if the current flowing into the bulb exceeds the maximum value, it will revert to its last successful value. Notice there’s a helpful oldValue constant available in didSet to access the previous value.
Give it a try:
var light = LightBulb()
light.current = 50
light.current // 0
light.current = 40
light.current // 40
When you try to set the light bulb to 50 amps, the bulb rejects that input. Pretty cool!
Note: Do not confuse property observers with getters and setters. A stored property can have a
didSetand awillSetobserver. A computed property has a getter and, optionally, a setter. These, even though the syntax is similar, are entirely different concepts!
Mini-Exercise
In the light bulb example, the bulb goes back to a successful setting if the current gets too high. In real life, that wouldn’t work, and the bulb would burn out! Your task is to rewrite the structure so the bulb turns off before the current burns it out.
Hint: You’ll need to use the willSet observer that gets called before the value is changed. The value about to be set is available in the constant newValue. The trick is that you can’t change this newValue, and it will still be set, so you’ll have to go beyond adding a willSet observer. :]
Lazy Properties
If you have a property that might take some time to calculate, you don’t want to slow things down until you need the property. Say hello to the lazy stored property. It is useful for such things as downloading a user’s profile picture or making a serious calculation.
Look at this example of a Circle structure that uses pi in its circumference calculation:
struct Circle {
lazy var pi = {
((4.0 * atan(1.0 / 5.0)) - atan(1.0 / 239.0)) * 4.0
}()
var radius = 0.0
var circumference: Double {
mutating get {
pi * radius * 2
}
}
init(radius: Double) {
self.radius = radius
}
}
For the sake of this example, you’re not using the value of pi available from the standard library; you calculate it explicitly.
You can create a new Circle with its initializer, and the pi calculation won’t run yet:
var circle = Circle(radius: 5) // got a circle, pi has not been run
The calculation of pi defers until you need it. Only when you ask for the circumference property is pi calculated and assigned a value.
circle.circumference // 31.42
// also, pi now has a value
Since you’ve got eagle eyes, you’ve noticed that pi uses a { }() self-executing closure pattern to calculate its value, even though it’s a stored property. The trailing parentheses execute the code inside the closure curly braces immediately. But since pi is marked as lazy, this calculation is postponed until the first time you access the property.
For comparison, circumference is a computed property calculated every time it’s accessed. You expect the circumference’s value to change if the radius changes. pi, as a lazy stored property, is only calculated the first time. That’s great because who wants to calculate the same thing repeatedly?
The lazy property must be a variable, defined with var, instead of a constant defined with let. When you first initialize the structure, the property effectively has no value. Then when some part of your code requests the property, its value will be calculated. So even though the value only changes once, you still use var.
Here are two more advanced features of the code:
- Since the value of
pichanges, thecircumferencegetter must be marked asmutating. Accessing the value ofpichanges the value of the structure. - Since
piis a stored property of the structure, you need a custom initializer to use only theradius. Remember, a structure’s automatic memberwise initializer includes all the stored properties.
In the next chapter, “Methods”, you’ll learn about the mutating keyword and custom initializers. The important concept to understand here is how the lazy stored property works. The rest of the details are window dressing that you’ll get more comfortable with in time.
Note:
lazyis a kind of property wrapper. By historical accident,lazyomits the@symbol and capitalization that usually prefix property wrappers. When you build apps with SwiftUI, you will see many other property wrappers, like@State,@Binding, and@EnvironmentObject. When you apply a property wrapper to a property, it gives that property some additional behavior. In Swift Apprentice: Beyond the Basics, you will learn how to make your own custom property wrappers.
Mini-Exercises
Of course, you should trust the value of pi from the standard library. It’s a type property, and you can access it as Double.pi. Given the Circle example above:
- Remove the lazy stored property
pi. Use the value of pi from the Swift standard library instead. - Remove the initializer. Since
radiusis the only stored property now, you can rely on the automatically included memberwise initializer.
Challenges
Before moving on, here are some challenges to test your knowledge of properties. It is best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.
Challenge 1: Ice Cream
Rewrite the IceCream structure below to use default values and lazy initialization:
struct IceCream {
let name: String
let ingredients: [String]
}
- Use default values for the properties.
- Lazily initialize the
ingredientsarray.
Challenge 2: Car and Fuel Tank
At the beginning of the chapter, you saw a Car structure. Dive into the inner workings of the car and rewrite the FuelTank structure below with property observer functionality:
struct FuelTank {
var level: Double // decimal percentage between 0 and 1
}
- Add a
lowFuelBoolean stored property to the structure. - Flip the
lowFuelBoolean when theleveldrops below 10%. - Ensure that when the tank fills back up, the
lowFuelwarning will turn off. - Set the
levelto a minimum of0or a maximum of1if it gets set above or below the expected values. - Add a
FuelTankproperty toCar.
Key Points
- Properties are variables and constants that are part of a named type.
- Stored properties allocate memory to store a value.
- Computed properties are calculated each time your code requests them and aren’t stored as a value in memory.
- The static modifier marks a type property that’s universal to all instances of a particular type.
- The lazy modifier prevents a value of a stored property from being calculated until your code uses it for the first time. You’ll want to use lazy initialization when a property’s initial value is computationally intensive or when you won’t know the initial value of a property until after you’ve initialized the object.