Home iOS & Swift Books Swift Apprentice

11
Properties Written by Ben Morrow

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
}

The values inside a stucture 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; 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 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 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:

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. When Grace married, she changed her last name:

person.fullName = "Grace Hopper"
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)
tv.diagonal // 110
tv.width = tv.height
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
tv.height // 34.32...
tv.width // 61.01...

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.

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
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
light.current // 0
light.current = 40
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
circle.circumference // 31.42
// also, pi now has a value

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:

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

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

  • 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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

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

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.