Chapters

Hide chapters

Swift Apprentice: Fundamentals

First Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section III: Building Your Own Types

Section 3: 9 chapters
Show chapters Hide chapters

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.

Contact fullName emailAddress

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:

  1. You use an Int type for your diagonal property. Although height and width are Double types, 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.
  2. 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 rounded method to round the value with the standard rule: If the decimal is 0.5 or above, it rounds up; otherwise, it rounds down.
  3. Now that you’ve got a properly-rounded number, you return it as an Int. Had you converted result directly to Int without 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:

  1. 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 get or set. This specificity isn’t required for read-only computed properties, as their single code block is implicitly a getter.
  2. You use the same code as before to get the computed value.
  3. 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.
  4. 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 newValue constant lets you use whatever value was passed in during the assignment.
  • Remember, the newValue is an Int, so to use it in a calculation with a Double, you must first convert it to a Double.
  • Once you’ve done the calculations, you assign the height and width properties of the TV structure.

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 unlocked from inside the didSet observer. Remember that didSet gets 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.highestLevel rather than just highestLevel to indicate you’re accessing a type property. You can also refer to the static property from within the type as Self.highestLevel. Using Self here is preferred because even if you change the name of the type to something else — say, GameLevel — the code would still work. The uppercase Self indicates 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 didSet and a willSet observer. 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.

Radius C i r c u m f e r e n c e

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 pi changes, the circumference getter must be marked as mutating. Accessing the value of pi changes the value of the structure.
  • Since pi is a stored property of the structure, you need a custom initializer to use only the radius. 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: lazy is a kind of property wrapper. By historical accident, lazy omits 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:

  1. Remove the lazy stored property pi. Use the value of pi from the Swift standard library instead.
  2. Remove the initializer. Since radius is 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]
}
  1. Use default values for the properties.
  2. Lazily initialize the ingredients array.

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
}
  1. Add a lowFuel Boolean stored property to the structure.
  2. Flip the lowFuel Boolean when the level drops below 10%.
  3. Ensure that when the tank fills back up, the lowFuel warning will turn off.
  4. Set the level to a minimum of 0 or a maximum of 1 if it gets set above or below the expected values.
  5. Add a FuelTank property to Car.

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.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.