Chapters

Hide chapters

Swift Apprentice

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Advanced Topics

Section 4: 10 chapters
Show chapters Hide chapters

11. Properties
Written by Ben Morrow

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the last chapter, you learned that structures make you a more efficient programmer by grouping related properties and behaviors into structured types.

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
}

Values like these are called properties. The two properties of Car are both 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, rather 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 initialization of a stored property.

Stored properties

As you may have guessed from the example in the introduction, you’re already familiar with many of the features of stored properties.

To review, imagine you’re building an address book. The common unit you’ll need is a Contact.

struct Contact {
  var fullName: String
  var emailAddress: String
}

You can use this structure over and over again, 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 one but opt not to assign a default value, because you plan to assign the value upon initialization. After all, the values will be different 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:

let name = person.fullName // Grace Murray
let email = 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. When Grace married, she changed her last name:

person.fullName = "Grace Hopper"
let grace = person.fullName // Grace Hopper

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 what the value of a property should be when the type is initialized, you can give that property a default value.

struct Contact {
  var fullName: String
  let emailAddress: String
  var relationship = "Friend"
}
var person = Contact(fullName: "Grace Murray",
                     emailAddress: "grace@navy.mil")
person.relationship // friend

var boss = Contact(fullName: "Ray Wenderlich",
                   emailAddress: "ray@raywenderlich.com",
                   relationship: "Boss")                     

Computed properties

Stored properties are certainly the most common, but there are also properties that are computed, which simply means they perform a calculation before returning a value.

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)
  }
}
var tv = TV(height: 53.93, width: 95.87)
let size = tv.diagonal // 110
tv.width = tv.height
let diagonal = tv.diagonal // 76

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 a called a read-only computed property. It has a block of code to compute the value of the property, called the getter.

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
  }
}
tv.diagonal = 70
let height = tv.height // 34.32...
let width = tv.width // 61.01...

Type properties

In the previous section, you learned how to associate stored and computed properties with instances of a particular type. The properties on your instance of TV are separate from the properties on my instance of TV.

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)
struct Level {
  static var highestLevel = 1
  let id: Int
  var boss: String
  var unlocked: Bool
}
// Error: you can’t access a type property on an instance
let highestLevel = level3.highestLevel
let highestLevel = Level.highestLevel // 1

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.

struct Level {
  static var highestLevel = 1
  let id: Int
  var boss: String
  var unlocked: Bool {
    didSet {
      if unlocked && id > Self.highestLevel {
        Self.highestLevel = id
      }
    }
  }
}

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
      }
    }
  }
}
var light = LightBulb()
light.current = 50
var current = light.current // 0
light.current = 40
current = light.current // 40

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. The bulb would burn out!

Lazy properties

If you have a property that might take some time to calculate, you don’t want to slow things down until you actually 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.

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

var circle = Circle(radius: 5) // got a circle, pi has not been run
let circumference = circle.circumference // 31.42
// also, pi now has a value

Mini-exercises

Of course, you should definitely 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:

Challenges

Before moving on, here are some challenges to test your knowledge of properties. It is best if you 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]
}

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
}

Key points

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.
© 2023 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.

Unlock now